Browse Source

SONAR-9064 Rework facets sidebar on the issues page

tags/6.4-RC1
Stas Vilchik 7 years ago
parent
commit
139261bbc1
100 changed files with 3279 additions and 2928 deletions
  1. 3
    3
      it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java
  2. 0
    18
      it/it-tests/src/test/java/it/issue/IssueSearchTest.java
  3. 3
    3
      it/it-tests/src/test/java/it/ui/UiTest.java
  4. 3
    14
      it/it-tests/src/test/java/pageobjects/issues/IssuesPage.java
  5. 0
    88
      it/it-tests/src/test/resources/issue/IssueSearchTest/bulk_change.html
  6. 0
    83
      it/it-tests/src/test/resources/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html
  7. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
  8. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
  9. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java
  10. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt
  11. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt
  12. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt
  13. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt
  14. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt
  15. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt
  16. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt
  17. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt
  18. 1
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt
  19. 0
    1
      server/sonar-web/.eslintrc
  20. 1
    2
      server/sonar-web/config/webpack/webpack.config.base.js
  21. 2
    0
      server/sonar-web/package.json
  22. 3
    0
      server/sonar-web/src/main/js/api/components.js
  23. 4
    11
      server/sonar-web/src/main/js/api/issues.js
  24. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
  25. 6
    3
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
  26. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
  27. 2
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap
  28. 8
    5
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
  29. 1
    0
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js
  30. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/SearchView.js
  31. 3
    1
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js
  32. 8
    2
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
  33. 4
    5
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  34. 3
    3
      server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
  35. 3
    3
      server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
  36. 1
    1
      server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
  37. 1
    1
      server/sonar-web/src/main/js/apps/code/components/ComponentName.js
  38. 1
    0
      server/sonar-web/src/main/js/apps/coding-rules/controller.js
  39. 2
    1
      server/sonar-web/src/main/js/apps/coding-rules/init.js
  40. 1
    0
      server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
  41. 1
    0
      server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
  42. 0
    130
      server/sonar-web/src/main/js/apps/component-issues/init.js
  43. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
  44. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
  45. 0
    303
      server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js
  46. 0
    60
      server/sonar-web/src/main/js/apps/issues/HeaderView.js
  47. 0
    132
      server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
  48. 649
    0
      server/sonar-web/src/main/js/apps/issues/components/App.js
  49. 27
    25
      server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
  50. 522
    0
      server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
  51. 72
    0
      server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js
  52. 25
    25
      server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
  53. 106
    0
      server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
  54. 62
    0
      server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
  55. 68
    0
      server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
  56. 106
    0
      server/sonar-web/src/main/js/apps/issues/components/ListItem.js
  57. 60
    0
      server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js
  58. 77
    0
      server/sonar-web/src/main/js/apps/issues/components/PageActions.js
  59. 122
    0
      server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js
  60. 49
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js
  61. 48
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap
  62. 0
    217
      server/sonar-web/src/main/js/apps/issues/controller.js
  63. 0
    63
      server/sonar-web/src/main/js/apps/issues/facets-view.js
  64. 0
    135
      server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js
  65. 0
    60
      server/sonar-web/src/main/js/apps/issues/facets/author-facet.js
  66. 0
    41
      server/sonar-web/src/main/js/apps/issues/facets/base-facet.js
  67. 0
    32
      server/sonar-web/src/main/js/apps/issues/facets/context-facet.js
  68. 0
    176
      server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js
  69. 0
    85
      server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js
  70. 0
    61
      server/sonar-web/src/main/js/apps/issues/facets/file-facet.js
  71. 0
    40
      server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js
  72. 0
    84
      server/sonar-web/src/main/js/apps/issues/facets/language-facet.js
  73. 0
    46
      server/sonar-web/src/main/js/apps/issues/facets/module-facet.js
  74. 0
    112
      server/sonar-web/src/main/js/apps/issues/facets/project-facet.js
  75. 0
    61
      server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js
  76. 0
    65
      server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js
  77. 0
    95
      server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js
  78. 0
    31
      server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js
  79. 0
    31
      server/sonar-web/src/main/js/apps/issues/facets/status-facet.js
  80. 0
    72
      server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js
  81. 0
    31
      server/sonar-web/src/main/js/apps/issues/facets/type-facet.js
  82. 0
    87
      server/sonar-web/src/main/js/apps/issues/init.js
  83. 0
    40
      server/sonar-web/src/main/js/apps/issues/issue-filter-view.js
  84. 0
    62
      server/sonar-web/src/main/js/apps/issues/layout.js
  85. 0
    30
      server/sonar-web/src/main/js/apps/issues/models/issue.js
  86. 0
    108
      server/sonar-web/src/main/js/apps/issues/models/issues.js
  87. 0
    81
      server/sonar-web/src/main/js/apps/issues/models/state.js
  88. 55
    0
      server/sonar-web/src/main/js/apps/issues/redirects.js
  89. 0
    35
      server/sonar-web/src/main/js/apps/issues/router.js
  90. 8
    5
      server/sonar-web/src/main/js/apps/issues/routes.js
  91. 168
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
  92. 99
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
  93. 276
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
  94. 120
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
  95. 67
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js
  96. 116
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
  97. 114
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
  98. 68
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js
  99. 113
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
  100. 0
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js

+ 3
- 3
it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java View File

@@ -156,14 +156,14 @@ public class IssueNotificationsTest extends AbstractIssueTest {
assertThat((String) message.getContent()).contains("Severity");
assertThat((String) message.getContent()).contains("One Issue Per Line (xoo): 17");
assertThat((String) message.getContent()).contains(
"See it in SonarQube: http://localhost:9000/component_issues?id=sample#createdAt=2015-12-15T00%3A00%3A00%2B");
"See it in SonarQube: http://localhost:9000/project/issues?id=sample&createdAt=2015-12-15T00%3A00%3A00%2B");

assertThat(emails.hasNext()).isTrue();
message = emails.next().getMimeMessage();
assertThat(message.getHeader("To", null)).isEqualTo("<tester@example.org>");
assertThat((String) message.getContent()).contains("sample/Sample.xoo");
assertThat((String) message.getContent()).contains("Assignee changed to Tester");
assertThat((String) message.getContent()).contains("See it in SonarQube: http://localhost:9000/issues/search#issues=" + issue.key());
assertThat((String) message.getContent()).contains("See it in SonarQube: http://localhost:9000/issues?issues=" + issue.key());

assertThat(emails.hasNext()).isFalse();
}
@@ -218,7 +218,7 @@ public class IssueNotificationsTest extends AbstractIssueTest {
assertThat((String) message.getContent()).contains("sample/Sample.xoo");
assertThat((String) message.getContent()).contains("Severity: BLOCKER (was MINOR)");
assertThat((String) message.getContent()).contains(
"See it in SonarQube: http://localhost:9000/issues/search#issues=" + issue.key());
"See it in SonarQube: http://localhost:9000/issues?issues=" + issue.key());

assertThat(emails.hasNext()).isFalse();
}

+ 0
- 18
it/it-tests/src/test/java/it/issue/IssueSearchTest.java View File

@@ -28,7 +28,6 @@ import org.apache.commons.lang.time.DateUtils;
import org.assertj.core.api.Fail;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.sonar.wsclient.base.HttpException;
import org.sonar.wsclient.base.Paging;
@@ -50,7 +49,6 @@ import static util.ItUtils.runProjectAnalysis;
import static util.ItUtils.setServerProperty;
import static util.ItUtils.toDate;
import static util.ItUtils.verifyHttpException;
import static util.selenium.Selenese.runSelenese;

public class IssueSearchTest extends AbstractIssueTest {

@@ -273,17 +271,6 @@ public class IssueSearchTest extends AbstractIssueTest {
assertThat(issues.component(issue).projectId()).isEqualTo(project.id());
}

/**
* SONAR-5659
*/
@Test
@Ignore("unstable")
public void redirect_to_search_url_after_wrong_login() {
// Force user authentication to check login on the issues search page
setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", "true");
runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html");
}

@Test
public void return_issue_type() throws Exception {
List<org.sonarqube.ws.Issues.Issue> issues = searchByRuleKey("xoo:OneBugIssuePerLine");
@@ -309,11 +296,6 @@ public class IssueSearchTest extends AbstractIssueTest {
assertThat(searchIssues(new SearchWsRequest().setTypes(singletonList("VULNERABILITY"))).getPaging().getTotal()).isEqualTo(8);
}

@Test
public void bulk_change() {
runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/bulk_change.html");
}

private List<org.sonarqube.ws.Issues.Issue> searchByRuleKey(String... ruleKey) throws IOException {
return searchIssues(new SearchWsRequest().setRules(asList(ruleKey))).getIssuesList();
}

+ 3
- 3
it/it-tests/src/test/java/it/ui/UiTest.java View File

@@ -87,14 +87,14 @@ public class UiTest {
$(".overview-quality-gate")
.shouldBe(visible)
.shouldHave(text("Passed"));
$("a[href=\"/component_issues?id=sample#resolved=false|types=CODE_SMELL\"]")
$("a[href=\"/project/issues?id=sample&resolved=false&types=CODE_SMELL\"]")
.shouldBe(visible)
.shouldHave(text("0"))
.click();

// on project issues page
assertThat(url()).contains("/component_issues?id=sample#resolved=false|types=CODE_SMELL");
$(".facet.active[data-unresolved]").shouldBe(visible);
assertThat(url()).contains("/project/issues?id=sample&resolved=false&types=CODE_SMELL");
$("[data-property=\"resolutions\"] .facet.active").shouldBe(visible);

$("#global-navigation").find("a[href=\"/profiles\"]").click();


+ 3
- 14
it/it-tests/src/test/java/pageobjects/issues/IssuesPage.java View File

@@ -19,9 +19,7 @@
*/
package pageobjects.issues;

import com.codeborne.selenide.CollectionCondition;
import com.codeborne.selenide.ElementsCollection;
import com.codeborne.selenide.SelenideElement;
import java.util.List;
import java.util.stream.Collectors;

@@ -55,23 +53,14 @@ public class IssuesPage {

public IssuesPage bulkChangeOpen() {
$("#issues-bulk-change").shouldBe(visible).click();
$("a.js-bulk-change").click();
$("#bulk-change-form").shouldBe(visible);
return this;
}

public IssuesPage bulkChangeAssigneeSearchCount(String query, Integer count) {
if (!$(".select2-drop-active").isDisplayed()) {
$("#bulk-change-form #s2id_assignee").shouldBe(visible).click();
}
SelenideElement input = $(".select2-drop-active input").shouldBe(visible);
input.val("").sendKeys(query);
if (count > 0) {
$(".select2-drop-active .select2-results li.select2-result").shouldBe(visible);
} else {
$(".select2-drop-active .select2-results li.select2-no-results").shouldBe(visible);
}
$$(".select2-drop-active .select2-results li.select2-result").shouldHaveSize(count);
$("#issues-bulk-change-assignee .Select-input input").val(query);
$$("#issues-bulk-change-assignee .Select-option").shouldHaveSize(count);
$("#issues-bulk-change-assignee .Select-input input").pressEscape();
return this;
}
}

+ 0
- 88
it/it-tests/src/test/resources/issue/IssueSearchTest/bulk_change.html View File

@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/sessions/new</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*Log In to SonarQube*</td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/issues</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.search-navigator-workspace-list .issue</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>id=issues-bulk-change</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>id=issues-bulk-change</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=#issues-bulk-change + .dropdown-menu .js-bulk-change</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#issues-bulk-change + .dropdown-menu .js-bulk-change</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>id=bulk-change-form</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>id=transition-confirm</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 83
it/it-tests/src/test/resources/issue/IssueSearchTest/redirect_to_search_url_after_wrong_login.html View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/logout</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/issues#resolved=true|statuses=OPEN</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*Log In to SonarQube*</td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>id=login</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>wrongpassword</td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>wronglogin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>wrongpassword</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=.alert</td>
<td>*Authentication failed*</td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>id=login</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>id=login</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>id=password</td>
<td>admin</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>assertLocation</td>
<td>*#resolved=true|statuses=OPEN*</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java View File

@@ -175,7 +175,7 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate {
String dateString = notification.getFieldValue(FIELD_PROJECT_DATE);
if (projectKey != null && dateString != null) {
Date date = DateUtils.parseDateTime(dateString);
String url = String.format("%s/component_issues?id=%s#createdAt=%s",
String url = String.format("%s/project/issues?id=%s&createdAt=%s",
settings.getServerBaseURL(), encode(projectKey), encode(DateUtils.formatDateTime(date)));
message
.append("See it in SonarQube: ")

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java View File

@@ -102,7 +102,7 @@ public class IssueChangesEmailTemplate extends EmailTemplate {

private void appendFooter(StringBuilder sb, Notification notification) {
String issueKey = notification.getFieldValue("key");
sb.append("See it in SonarQube: ").append(settings.getServerBaseURL()).append("/issues/search#issues=").append(issueKey).append(NEW_LINE);
sb.append("See it in SonarQube: ").append(settings.getServerBaseURL()).append("/issues?issues=").append(issueKey).append(NEW_LINE);
}

private static void appendLine(StringBuilder sb, @Nullable String line) {

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java View File

@@ -60,7 +60,7 @@ public class MyNewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate {
String assignee = notification.getFieldValue(FIELD_ASSIGNEE);
if (projectUuid != null && dateString != null && assignee != null) {
Date date = DateUtils.parseDateTime(dateString);
String url = String.format("%s/issues/search#projectUuids=%s|assignees=%s|createdAt=%s",
String url = String.format("%s/issues?projectUuids=%s&assignees=%s&createdAt=%s",
settings.getServerBaseURL(),
encode(projectUuid),
encode(assignee),

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt View File

@@ -3,4 +3,4 @@ Rule: Avoid Cycles
Message: Has 3 cycles


See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt View File

@@ -4,4 +4,4 @@ Message: Has 3 cycles

Resolution: FIXED (was FALSE-POSITIVE)

See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt View File

@@ -4,4 +4,4 @@ Message: Has 3 cycles

Action Plan changed to ABC 1.0

See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt View File

@@ -4,4 +4,4 @@ Message: Has 3 cycles

Assignee changed to louis

See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt View File

@@ -9,4 +9,4 @@ Resolution: FALSE-POSITIVE
Status: RESOLVED
Tags: [bug performance]

See it in SonarQube: http://nemo.sonarsource.org/issues/search#issues=ABCDE
See it in SonarQube: http://nemo.sonarsource.org/issues?issues=ABCDE

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt View File

@@ -17,4 +17,4 @@ Project: Struts
/path/to/file: 3
/path/to/directory: 7

See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1
See it in SonarQube: http://nemo.sonarsource.org/issues?projectUuids=ABCDE&assignees=lo.gin&createdAt=2010-05-1

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt View File

@@ -5,4 +5,4 @@ Project: Struts
Severity
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1

See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1
See it in SonarQube: http://nemo.sonarsource.org/issues?projectUuids=ABCDE&assignees=lo.gin&createdAt=2010-05-1

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt View File

@@ -21,4 +21,4 @@ Project: Struts
/path/to/file: 3
/path/to/directory: 7

See it in SonarQube: http://nemo.sonarsource.org/component_issues?id=org.apache%3Astruts#createdAt=2010-05-1
See it in SonarQube: http://nemo.sonarsource.org/project/issues?id=org.apache%3Astruts&createdAt=2010-05-1

+ 1
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt View File

@@ -5,4 +5,4 @@ Project: Struts
Severity
Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1

See it in SonarQube: http://nemo.sonarsource.org/component_issues?id=org.apache%3Astruts#createdAt=2010-05-1
See it in SonarQube: http://nemo.sonarsource.org/project/issues?id=org.apache%3Astruts&createdAt=2010-05-1

+ 0
- 1
server/sonar-web/.eslintrc View File

@@ -11,7 +11,6 @@
},

"globals": {
"key": true,
"baseUrl": true,
"SyntheticInputEvent": true
},

+ 1
- 2
server/sonar-web/config/webpack/webpack.config.base.js View File

@@ -25,7 +25,6 @@ module.exports = {
'handlebars/runtime',
'./src/main/js/libs/third-party/jquery-ui.js',
'./src/main/js/libs/third-party/select2.js',
'./src/main/js/libs/third-party/keymaster.js',
'./src/main/js/libs/third-party/bootstrap/tooltip.js',
'./src/main/js/libs/third-party/bootstrap/dropdown.js'
],
@@ -92,7 +91,7 @@ module.exports = {
{ test: require.resolve('react-dom'), loader: 'expose?ReactDOM' }
]
},
postcss: function() {
postcss() {
return [autoprefixer(autoprefixerOptions)];
},
// Some libraries import Node modules but don't use them in the browser.

+ 2
- 0
server/sonar-web/package.json View File

@@ -17,9 +17,11 @@
"d3-selection": "1.0.5",
"d3-shape": "1.0.6",
"escape-html": "1.0.3",
"glamor": "2.20.24",
"handlebars": "2.0.0",
"history": "2.0.0",
"jquery": "2.2.0",
"keymaster": "1.6.2",
"lodash": "4.6.1",
"moment": "2.10.6",
"numeral": "1.5.3",

+ 3
- 0
server/sonar-web/src/main/js/api/components.js View File

@@ -137,6 +137,9 @@ export function searchProjects(data?: Object) {
return getJSON(url, data);
}

export const searchComponents = (data?: { q?: string, qualifiers?: string, ps?: number }) =>
getJSON('/api/components/search', data);

/**
* Change component's key
* @param {string} from

+ 4
- 11
server/sonar-web/src/main/js/api/issues.js View File

@@ -41,15 +41,6 @@ type IssuesResponse = {
users?: Array<*>
};

export type Transition =
| 'confirm'
| 'unconfirm'
| 'reopen'
| 'resolve'
| 'falsepositive'
| 'wontfix'
| 'close';

export const searchIssues = (query: {}): Promise<IssuesResponse> =>
getJSON('/api/issues/search', query);

@@ -97,7 +88,9 @@ export function getIssuesCount(query: {}): Promise<*> {
});
}

export const searchIssueTags = (ps: number = 500) => getJSON('/api/issues/tags', { ps });
export const searchIssueTags = (
data: { ps?: number, q?: string } = { ps: 500 }
): Promise<Array<string>> => getJSON('/api/issues/tags', data).then(r => r.tags);

export function getIssueChangelog(issue: string): Promise<*> {
const url = '/api/issues/changelog';
@@ -142,7 +135,7 @@ export function setIssueTags(data: { issue: string, tags: string }): Promise<Iss
}

export function setIssueTransition(
data: { issue: string, transition: Transition }
data: { issue: string, transition: string }
): Promise<IssueResponse> {
const url = '/api/issues/do_transition';
return postJSON(url, data);

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js View File

@@ -20,7 +20,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import QualifierIcon from '../../../../components/shared/qualifier-icon';
import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
import OrganizationLink from '../../../../components/ui/OrganizationLink';


+ 6
- 3
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js View File

@@ -101,11 +101,14 @@ export default class ComponentNavMenu extends React.Component {
);
}

renderComponentIssuesLink() {
renderIssuesLink() {
return (
<li>
<Link
to={{ pathname: '/component_issues', query: { id: this.props.component.key } }}
to={{
pathname: '/project/issues',
query: { id: this.props.component.key, resolved: 'false' }
}}
activeClassName="active">
{translate('issues.page')}
</Link>
@@ -343,7 +346,7 @@ export default class ComponentNavMenu extends React.Component {
return (
<ul className="nav navbar-nav nav-tabs">
{this.renderDashboardLink()}
{this.renderComponentIssuesLink()}
{this.renderIssuesLink()}
{this.renderComponentMeasuresLink()}
{this.renderCodeLink()}
{this.renderActivityLink()}

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap View File

@@ -4,7 +4,7 @@ exports[`test should not render breadcrumbs with one element 1`] = `
<span>
<span
className="navbar-context-title-qualifier little-spacer-right">
<qualifier-icon
<QualifierIcon
qualifier="TRK" />
</span>
<Link
@@ -33,7 +33,7 @@ exports[`test should render organization 1`] = `
<span>
<span
className="navbar-context-title-qualifier little-spacer-right">
<qualifier-icon
<QualifierIcon
qualifier="TRK" />
</span>
<OrganizationLink

+ 2
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap View File

@@ -25,9 +25,10 @@ exports[`test should work with extensions 1`] = `
style={Object {}}
to={
Object {
"pathname": "/component_issues",
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"resolved": "false",
},
}
}>

+ 8
- 5
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js View File

@@ -25,7 +25,10 @@ import { isUserAdmin } from '../../../../helpers/users';
export default class GlobalNavMenu extends React.Component {
static propTypes = {
appState: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired
currentUser: React.PropTypes.object.isRequired,
location: React.PropTypes.shape({
pathname: React.PropTypes.string.isRequired
}).isRequired
};

static defaultProps = {
@@ -59,12 +62,12 @@ export default class GlobalNavMenu extends React.Component {

renderIssuesLink() {
const query = this.props.currentUser.isLoggedIn
? '#resolved=false|assigned_to_me=true'
: '#resolved=false';
const url = '/issues' + query;
? { myIssues: 'true', resolved: 'false' }
: { resolved: 'false' };
const active = this.props.location.pathname === 'issues';
return (
<li>
<Link to={url} className={this.activeLink('/issues')}>
<Link to={{ pathname: '/issues', query }} className={active ? 'active' : undefined}>
{translate('issues.page')}
</Link>
</li>

+ 1
- 0
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearch.js View File

@@ -20,6 +20,7 @@
import Backbone from 'backbone';
import React from 'react';
import { connect } from 'react-redux';
import key from 'keymaster';
import SearchView from './SearchView';
import { getCurrentUser } from '../../../../store/rootReducer';


+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/SearchView.js View File

@@ -253,7 +253,7 @@ export default Marionette.LayoutView.extend({

getNavigationFindings(q) {
const DEFAULT_ITEMS = [
{ name: translate('issues.page'), url: window.baseUrl + '/issues/search' },
{ name: translate('issues.page'), url: window.baseUrl + '/issues' },
{
name: translate('layout.measures'),
url: window.baseUrl + '/measures/search?qualifiers[]=TRK'

+ 3
- 1
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js View File

@@ -30,6 +30,8 @@ it('should work with extensions', () => {
isLoggedIn: false,
permissions: { global: [] }
};
const wrapper = shallow(<GlobalNavMenu appState={appState} currentUser={currentUser} />);
const wrapper = shallow(
<GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} />
);
expect(wrapper).toMatchSnapshot();
});

+ 8
- 2
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap View File

@@ -12,10 +12,16 @@ exports[`test should work with extensions 1`] = `
</li>
<li>
<Link
className={null}
onlyActiveOnIndex={false}
style={Object {}}
to="/issues#resolved=false">
to={
Object {
"pathname": "/issues",
"query": Object {
"resolved": "false",
},
}
}>
issues.page
</Link>
</li>

+ 4
- 5
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -44,7 +44,6 @@ import backgroundTasksRoutes from '../../apps/background-tasks/routes';
import codeRoutes from '../../apps/code/routes';
import codingRulesRoutes from '../../apps/coding-rules/routes';
import componentRoutes from '../../apps/component/routes';
import componentIssuesRoutes from '../../apps/component-issues/routes';
import componentMeasuresRoutes from '../../apps/component-measures/routes';
import customMeasuresRoutes from '../../apps/custom-measures/routes';
import groupsRoutes from '../../apps/groups/routes';
@@ -89,9 +88,8 @@ const startReactApp = () => {
<Router history={history} onUpdate={handleUpdate}>
<Route
path="/account/issues"
onEnter={() => {
const defaultFilter = window.location.hash || '#resolve=false';
window.location = `${window.baseUrl}/issues${defaultFilter}|assigned_to_me=true`;
onEnter={(_, replace) => {
replace({ pathname: '/issues', query: { myIssues: 'true', resolved: 'false' } });
}}
/>

@@ -117,6 +115,7 @@ const startReactApp = () => {
/>

<Redirect from="/component/index" to="/component" />
<Redirect from="/component_issues" to="/project/issues" />
<Redirect from="/dashboard/index" to="/dashboard" />
<Redirect from="/governance" to="/view" />
<Redirect from="/extension/governance/portfolios" to="/portfolios" />
@@ -158,7 +157,6 @@ const startReactApp = () => {

<Route component={ProjectContainer}>
<Route path="code" childRoutes={codeRoutes} />
<Route path="component_issues" childRoutes={componentIssuesRoutes} />
<Route path="component_measures" childRoutes={componentMeasuresRoutes} />
<Route path="custom_measures" childRoutes={customMeasuresRoutes} />
<Route path="dashboard" childRoutes={overviewRoutes} />
@@ -176,6 +174,7 @@ const startReactApp = () => {
component={ProjectPageExtension}
/>
<Route path="background_tasks" childRoutes={backgroundTasksRoutes} />
<Route path="issues" childRoutes={issuesRoutes} />
<Route path="settings" childRoutes={settingsRoutes} />
{projectAdminRoutes}
</Route>

+ 3
- 3
server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js View File

@@ -43,7 +43,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
to={getIssuesUrl({ resolved: false, types: 'BUG' })}
to={getIssuesUrl({ resolved: 'false', types: 'BUG' })}
className="about-page-issue-type-link">
{formatMeasure(bugs, 'SHORT_INT')}
</Link>
@@ -56,7 +56,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })}
to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })}
className="about-page-issue-type-link">
{formatMeasure(vulnerabilities, 'SHORT_INT')}
</Link>
@@ -69,7 +69,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })}
to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })}
className="about-page-issue-type-link">
{formatMeasure(codeSmells, 'SHORT_INT')}
</Link>

+ 3
- 3
server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js View File

@@ -43,7 +43,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
to={getIssuesUrl({ resolved: false, types: 'BUG' })}
to={getIssuesUrl({ resolved: 'false', types: 'BUG' })}
className="about-page-issue-type-link">
{formatMeasure(bugs, 'SHORT_INT')}
</Link>
@@ -56,7 +56,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })}
to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })}
className="about-page-issue-type-link">
{formatMeasure(vulnerabilities, 'SHORT_INT')}
</Link>
@@ -69,7 +69,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })}
to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })}
className="about-page-issue-type-link">
{formatMeasure(codeSmells, 'SHORT_INT')}
</Link>

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js View File

@@ -21,7 +21,7 @@
import React from 'react';
import { Link } from 'react-router';
import TaskType from './TaskType';
import QualifierIcon from '../../../components/shared/qualifier-icon';
import QualifierIcon from '../../../components/shared/QualifierIcon';
import Organization from '../../../components/shared/Organization';
import { Task } from '../types';


+ 1
- 1
server/sonar-web/src/main/js/apps/code/components/ComponentName.js View File

@@ -20,7 +20,7 @@
import React from 'react';
import { Link } from 'react-router';
import Truncated from './Truncated';
import QualifierIcon from '../../../components/shared/qualifier-icon';
import QualifierIcon from '../../../components/shared/QualifierIcon';

function getTooltip(component) {
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';

+ 1
- 0
server/sonar-web/src/main/js/apps/coding-rules/controller.js View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import key from 'keymaster';
import Controller from '../../components/navigator/controller';
import Rule from './models/rule';
import RuleDetailsView from './rule-details-view';

+ 2
- 1
server/sonar-web/src/main/js/apps/coding-rules/init.js View File

@@ -22,6 +22,7 @@ import $ from 'jquery';
import { sortBy } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
import key from 'keymaster';
import State from './models/state';
import Layout from './layout';
import Rules from './models/rules';
@@ -105,7 +106,7 @@ App.on('start', function(options: {
});
this.layout.filtersRegion.show(this.filtersView);

window.key.setScope('list');
key.setScope('list');
this.router = new Router({
app: this
});

+ 1
- 0
server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js View File

@@ -21,6 +21,7 @@ import $ from 'jquery';
import { union } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
import key from 'keymaster';
import Rules from './models/rules';
import MetaView from './rule/rule-meta-view';
import DescView from './rule/rule-description-view';

+ 1
- 0
server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js View File

@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import key from 'keymaster';
import WorkspaceListView from '../../components/navigator/workspace-list-view';
import WorkspaceListItemView from './workspace-list-item-view';
import WorkspaceListEmptyView from './workspace-list-empty-view';

+ 0
- 130
server/sonar-web/src/main/js/apps/component-issues/init.js View File

@@ -1,130 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import { difference } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
import State from '../issues/models/state';
import Layout from '../issues/layout';
import Issues from '../issues/models/issues';
import Facets from '../../components/navigator/models/facets';
import Controller from '../issues/controller';
import Router from '../issues/router';
import WorkspaceListView from '../issues/workspace-list-view';
import WorkspaceHeaderView from '../issues/workspace-header-view';
import FacetsView from './../issues/facets-view';
import HeaderView from './../issues/HeaderView';

const App = new Marionette.Application();
const init = function({ el, component, currentUser }) {
this.config = {
resource: component.id,
resourceName: component.name,
resourceQualifier: component.qualifier
};
this.state = new State({
canBulkChange: currentUser.isLoggedIn,
isContext: true,
contextQuery: { componentUuids: this.config.resource },
contextComponentUuid: this.config.resource,
contextComponentName: this.config.resourceName,
contextComponentQualifier: this.config.resourceQualifier,
contextOrganization: component.organization
});
this.updateContextFacets();
this.list = new Issues();
this.facets = new Facets();

this.layout = new Layout({ app: this, el });
this.layout.render();
$('#footer').addClass('search-navigator-footer');

this.controller = new Controller({ app: this });

this.issuesView = new WorkspaceListView({
app: this,
collection: this.list
});
this.layout.workspaceListRegion.show(this.issuesView);
this.issuesView.bindScrollEvents();

this.workspaceHeaderView = new WorkspaceHeaderView({
app: this,
collection: this.list
});
this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);

this.facetsView = new FacetsView({
app: this,
collection: this.facets
});
this.layout.facetsRegion.show(this.facetsView);

this.headerView = new HeaderView({
app: this
});
this.layout.filtersRegion.show(this.headerView);

key.setScope('list');
App.router = new Router({ app: App });
Backbone.history.start();
};

App.getContextQuery = function() {
return { componentUuids: this.config.resource };
};

App.getRestrictedFacets = function() {
return {
TRK: ['projectUuids'],
BRC: ['projectUuids'],
DIR: ['projectUuids', 'moduleUuids', 'directories'],
DEV: ['authors'],
DEV_PRJ: ['projectUuids', 'authors']
};
};

App.updateContextFacets = function() {
const facets = this.state.get('facets');
const allFacets = this.state.get('allFacets');
const facetsFromServer = this.state.get('facetsFromServer');
return this.state.set({
facets,
allFacets: difference(allFacets, this.getRestrictedFacets()[this.config.resourceQualifier]),
facetsFromServer: difference(
facetsFromServer,
this.getRestrictedFacets()[this.config.resourceQualifier]
)
});
};

App.on('start', options => {
init.call(App, options);
});

export default function(el, component, currentUser) {
App.start({ el, component, currentUser });

return () => {
Backbone.history.stop();
App.layout.destroy();
$('#footer').removeClass('search-navigator-footer');
};
}

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import QualifierIcon from '../../../../components/shared/qualifier-icon';
import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { isDiffMetric, formatLeak } from '../../utils';
import { formatMeasure } from '../../../../helpers/measures';


+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js View File

@@ -19,7 +19,7 @@
*/
import React from 'react';
import classNames from 'classnames';
import QualifierIcon from '../../../../components/shared/qualifier-icon';
import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { splitPath } from '../../../../helpers/path';
import { getComponentUrl } from '../../../../helpers/urls';


+ 0
- 303
server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js View File

@@ -1,303 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import { debounce, sortBy } from 'lodash';
import ModalForm from '../../components/common/modal-form';
import Template from './templates/BulkChangeForm.hbs';
import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore';
import { searchIssues, searchIssueTags, bulkChangeIssues } from '../../api/issues';
import { searchUsers } from '../../api/users';
import { searchMembers } from '../../api/organizations';
import { translate, translateWithParameters } from '../../helpers/l10n';

const LIMIT = 500;
const INPUT_WIDTH = '250px';
const MINIMUM_QUERY_LENGTH = 2;
const UNASSIGNED = '<UNASSIGNED>';

type Issue = {
actions?: Array<string>,
assignee: string | null,
transitions?: Array<string>
};

const hasAction = (action: string) =>
(issue: Issue) => issue.actions && issue.actions.includes(action);

export default ModalForm.extend({
template: Template,

initialize() {
this.issues = null;
this.paging = null;
this.tags = null;
this.loadIssues();
this.loadTags();
},

loadIssues() {
const { query } = this.options;
searchIssues({
...query,
additionalFields: 'actions,transitions',
ps: LIMIT
}).then(r => {
this.issues = r.issues;
this.paging = r.paging;
this.render();
});
},

loadTags() {
searchIssueTags().then(r => {
this.tags = r.tags;
this.render();
});
},

assigneeSearch(defaultOptions) {
const { context } = this.options;
return debounce(
query => {
if (query.term.length === 0) {
query.callback({ results: defaultOptions });
} else if (query.term.length >= MINIMUM_QUERY_LENGTH) {
const onSuccess = r => {
query.callback({
results: r.users.map(user => ({
id: user.login,
text: `${user.name} (${user.login})`
}))
});
};
if (context.isContext) {
searchMembers({ organization: context.organization, q: query.term }).then(onSuccess);
} else {
searchUsers(query.term).then(onSuccess);
}
}
},
250
);
},

prepareAssigneeSelect() {
const input = this.$('#assignee');
if (input.length) {
const canBeAssignedToMe = this.issues && this.canBeAssignedToMe(this.issues);
const currentUser = getCurrentUserFromStore();
const canBeUnassigned = this.issues && this.canBeUnassigned(this.issues);
const defaultOptions = [];
if (canBeAssignedToMe && currentUser.isLoggedIn) {
defaultOptions.push({
id: currentUser.login,
text: `${currentUser.name} (${currentUser.login})`
});
}
if (canBeUnassigned) {
defaultOptions.push({ id: UNASSIGNED, text: translate('unassigned') });
}

input.select2({
allowClear: false,
placeholder: translate('search_verb'),
width: INPUT_WIDTH,
formatNoMatches: () => translate('select2.noMatches'),
formatSearching: () => translate('select2.searching'),
formatInputTooShort: () =>
translateWithParameters('select2.tooShort', MINIMUM_QUERY_LENGTH),
query: this.assigneeSearch(defaultOptions)
});

input.on('change', () => this.$('#assign-action').prop('checked', true));
}
},

prepareTypeSelect() {
this.$('#type')
.select2({
minimumResultsForSearch: 999,
width: INPUT_WIDTH
})
.on('change', () => this.$('#set-type-action').prop('checked', true));
},

prepareSeveritySelect() {
const format = state =>
state.id
? `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${state.text}`
: state.text;
this.$('#severity')
.select2({
minimumResultsForSearch: 999,
width: INPUT_WIDTH,
formatResult: format,
formatSelection: format
})
.on('change', () => this.$('#set-severity-action').prop('checked', true));
},

prepareTagsInput() {
this.$('#add_tags')
.select2({
width: INPUT_WIDTH,
tags: this.tags
})
.on('change', () => this.$('#add-tags-action').prop('checked', true));

this.$('#remove_tags')
.select2({
width: INPUT_WIDTH,
tags: this.tags
})
.on('change', () => this.$('#remove-tags-action').prop('checked', true));
},

onRender() {
ModalForm.prototype.onRender.apply(this, arguments);
this.prepareAssigneeSelect();
this.prepareTypeSelect();
this.prepareSeveritySelect();
this.prepareTagsInput();
},

onFormSubmit() {
ModalForm.prototype.onFormSubmit.apply(this, arguments);
const query = {};

const assignee = this.$('#assignee').val();
if (this.$('#assign-action').is(':checked') && assignee != null) {
query['assign'] = assignee === UNASSIGNED ? '' : assignee;
}

const type = this.$('#type').val();
if (this.$('#set-type-action').is(':checked') && type) {
query['set_type'] = type;
}

const severity = this.$('#severity').val();
if (this.$('#set-severity-action').is(':checked') && severity) {
query['set_severity'] = severity;
}

const addedTags = this.$('#add_tags').val();
if (this.$('#add-tags-action').is(':checked') && addedTags) {
query['add_tags'] = addedTags;
}

const removedTags = this.$('#remove_tags').val();
if (this.$('#remove-tags-action').is(':checked') && removedTags) {
query['remove_tags'] = removedTags;
}

const transition = this.$('[name="do_transition.transition"]:checked').val();
if (transition) {
query['do_transition'] = transition;
}

const comment = this.$('#comment').val();
if (comment) {
query['comment'] = comment;
}

const sendNotifications = this.$('#send-notifications').is(':checked');
if (sendNotifications) {
query['sendNotifications'] = sendNotifications;
}

this.disableForm();
this.showSpinner();

const issueKeys = this.issues.map(issue => issue.key);
bulkChangeIssues(issueKeys, query).then(
() => {
this.destroy();
this.options.onChange();
},
(e: Object) => {
this.enableForm();
this.hideSpinner();
e.response.json().then(r => this.showErrors(r.errors, r.warnings));
}
);
},

canBeAssigned(issues: Array<Issue>) {
return issues.filter(hasAction('assign')).length;
},

canBeAssignedToMe(issues: Array<Issue>) {
return issues.filter(hasAction('assign_to_me')).length;
},

canBeUnassigned(issues: Array<Issue>) {
return issues.filter(issue => issue.assignee).length;
},

canChangeType(issues: Array<Issue>) {
return issues.filter(hasAction('set_type')).length;
},

canChangeSeverity(issues: Array<Issue>) {
return issues.filter(hasAction('set_severity')).length;
},

canChangeTags(issues: Array<Issue>) {
return issues.filter(hasAction('set_tags')).length;
},

canBeCommented(issues: Array<Issue>) {
return issues.filter(hasAction('comment')).length;
},

availableTransitions(issues: Array<Issue>) {
const transitions = {};
issues.forEach(issue => {
if (issue.transitions) {
issue.transitions.forEach(t => {
if (transitions[t] != null) {
transitions[t]++;
} else {
transitions[t] = 1;
}
});
}
});
return sortBy(Object.keys(transitions)).map(transition => ({
transition,
count: transitions[transition]
}));
},

serializeData() {
return {
...ModalForm.prototype.serializeData.apply(this, arguments),
isLoaded: this.issues != null && this.tags != null,
issues: this.issues,
limitReached: this.paging && this.paging.total > LIMIT,
canBeAssigned: this.issues && this.canBeAssigned(this.issues),
canChangeType: this.issues && this.canChangeType(this.issues),
canChangeSeverity: this.issues && this.canChangeSeverity(this.issues),
canChangeTags: this.issues && this.canChangeTags(this.issues),
canBeCommented: this.issues && this.canBeCommented(this.issues),
availableTransitions: this.issues && this.availableTransitions(this.issues)
};
}
});

+ 0
- 60
server/sonar-web/src/main/js/apps/issues/HeaderView.js View File

@@ -1,60 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import Marionette from 'backbone.marionette';
import Template from './templates/facets/issues-my-issues-facet.hbs';

export default Marionette.ItemView.extend({
template: Template,
className: 'issues-header-inner',

events: {
'change [name="issues-page-my"]': 'onMyIssuesChange'
},

initialize() {
this.listenTo(this.options.app.state, 'change:query', this.render);
},

onMyIssuesChange() {
const mode = this.$('[name="issues-page-my"]:checked').val();
if (mode === 'my') {
this.options.app.state.updateFilter({
assigned_to_me: 'true',
assignees: null,
assigned: null
});
} else {
this.options.app.state.updateFilter({
assigned_to_me: null,
assignees: null,
assigned: null
});
}
},
serializeData() {
const me = !!this.options.app.state.get('query').assigned_to_me;
return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
me,
isContext: this.options.app.state.get('isContext'),
user: this.options.app.state.get('user')
};
}
});

+ 0
- 132
server/sonar-web/src/main/js/apps/issues/component-viewer/main.js View File

@@ -1,132 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import Marionette from 'backbone.marionette';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import WithStore from '../../../components/shared/WithStore';

export default Marionette.ItemView.extend({
template() {
return '<div></div>';
},

initialize(options) {
this.handleLoadIssues = this.handleLoadIssues.bind(this);
this.scrollToBaseIssue = this.scrollToBaseIssue.bind(this);
this.selectIssue = this.selectIssue.bind(this);
this.listenTo(options.app.state, 'change:selectedIndex', this.select);
},

onRender() {
this.showViewer();
},

onDestroy() {
this.unbindShortcuts();
unmountComponentAtNode(this.el);
},

handleLoadIssues(component: string) {
// TODO fromLine: number, toLine: number
const issues = this.options.app.list.toJSON().filter(issue => issue.componentKey === component);
return Promise.resolve(issues);
},

showViewer(onLoaded) {
if (!this.baseIssue) {
return;
}

const componentKey = this.baseIssue.get('component');

render(
<WithStore>
<SourceViewer
aroundLine={this.baseIssue.get('line')}
component={componentKey}
displayAllIssues={true}
loadIssues={this.handleLoadIssues}
onLoaded={onLoaded}
onIssueSelect={this.selectIssue}
selectedIssue={this.baseIssue.get('key')}
/>
</WithStore>,
this.el
);
},

openFileByIssue(issue) {
this.baseIssue = issue;
this.selectedIssue = issue.get('key');
this.showViewer(this.scrollToBaseIssue);
this.bindShortcuts();
},

bindShortcuts() {
key('up', 'componentViewer', () => {
this.options.app.controller.selectPrev();
return false;
});
key('down', 'componentViewer', () => {
this.options.app.controller.selectNext();
return false;
});
key('left,backspace', 'componentViewer', () => {
this.options.app.controller.closeComponentViewer();
return false;
});
},

unbindShortcuts() {
key.deleteScope('componentViewer');
},

select() {
const selected = this.options.app.state.get('selectedIndex');
const selectedIssue = this.options.app.list.at(selected);

if (selectedIssue.get('component') === this.baseIssue.get('component')) {
this.baseIssue = selectedIssue;
this.showViewer(this.scrollToBaseIssue);
this.scrollToBaseIssue();
} else {
this.options.app.controller.showComponentViewer(selectedIssue);
}
},

scrollToLine(line) {
const row = this.$(`[data-line-number=${line}]`);
const topOffset = $(window).height() / 2 - 60;
const goal = row.length > 0 ? row.offset().top - topOffset : 0;
$(window).scrollTop(goal);
},

selectIssue(issueKey) {
const issue = this.options.app.list.find(model => model.get('key') === issueKey);
const index = this.options.app.list.indexOf(issue);
this.options.app.state.set({ selectedIndex: index });
},

scrollToBaseIssue() {
this.scrollToLine(this.baseIssue.get('line'));
}
});

+ 649
- 0
server/sonar-web/src/main/js/apps/issues/components/App.js View File

@@ -0,0 +1,649 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import Helmet from 'react-helmet';
import key from 'keymaster';
import { keyBy, without } from 'lodash';
import HeaderPanel from './HeaderPanel';
import PageActions from './PageActions';
import FiltersHeader from './FiltersHeader';
import MyIssuesFilter from './MyIssuesFilter';
import Sidebar from '../sidebar/Sidebar';
import IssuesList from './IssuesList';
import ComponentBreadcrumbs from './ComponentBreadcrumbs';
import IssuesSourceViewer from './IssuesSourceViewer';
import BulkChangeModal from './BulkChangeModal';
import {
parseQuery,
areMyIssuesSelected,
areQueriesEqual,
getOpen,
serializeQuery,
parseFacets
} from '../utils';
import type {
Query,
Paging,
Facet,
ReferencedComponent,
ReferencedUser,
ReferencedLanguage,
Component,
CurrentUser
} from '../utils';
import ListFooter from '../../../components/controls/ListFooter';
import EmptySearch from '../../../components/common/EmptySearch';
import Page from '../../../components/layout/Page';
import PageMain from '../../../components/layout/PageMain';
import PageMainInner from '../../../components/layout/PageMainInner';
import PageSide from '../../../components/layout/PageSide';
import PageFilters from '../../../components/layout/PageFilters';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { scrollToElement } from '../../../helpers/scrolling';
import type { Issue } from '../../../components/issue/types';

type Props = {
component?: Component,
currentUser: CurrentUser,
fetchIssues: () => Promise<*>,
location: { pathname: string, query: { [string]: string } },
onRequestFail: (Error) => void,
router: { push: () => void, replace: () => void }
};

type State = {
bulkChange: 'all' | 'selected' | null,
checked: Array<string>,
facets: { [string]: Facet },
issues: Array<Issue>,
loading: boolean,
myIssues: boolean,
openFacets: { [string]: boolean },
paging?: Paging,
query: Query,
referencedComponents: { [string]: ReferencedComponent },
referencedLanguages: { [string]: ReferencedLanguage },
referencedRules: { [string]: { name: string } },
referencedUsers: { [string]: ReferencedUser },
selected?: string
};

const DEFAULT_QUERY = { resolved: 'false' };

export default class App extends React.PureComponent {
mounted: boolean;
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = {
bulkChange: null,
checked: [],
facets: {},
issues: [],
loading: true,
myIssues: areMyIssuesSelected(props.location.query),
openFacets: { resolutions: true, types: true },
query: parseQuery(props.location.query),
referencedComponents: {},
referencedLanguages: {},
referencedRules: {},
referencedUsers: {},
selected: getOpen(props.location.query)
};
}

componentDidMount() {
this.mounted = true;

const footer = document.getElementById('footer');
if (footer) {
footer.classList.add('search-navigator-footer');
}

this.attachShortcuts();
this.fetchFirstIssues();
}

componentWillReceiveProps(nextProps: Props) {
const open = getOpen(nextProps.location.query);
if (open != null && open !== this.state.selected) {
this.setState({ selected: open });
}
this.setState({
myIssues: areMyIssuesSelected(nextProps.location.query),
query: parseQuery(nextProps.location.query)
});
}

componentDidUpdate(prevProps: Props, prevState: State) {
const { query } = this.props.location;
const { query: prevQuery } = prevProps.location;
if (
!areQueriesEqual(prevQuery, query) ||
areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
) {
this.fetchFirstIssues();
} else if (prevState.selected !== this.state.selected) {
const open = getOpen(query);
if (!open) {
this.scrollToSelectedIssue();
}
}
}

componentWillUnmount() {
this.detachShortcuts();

const footer = document.getElementById('footer');
if (footer) {
footer.classList.remove('search-navigator-footer');
}

this.mounted = false;
}

attachShortcuts() {
key.setScope('issues');
key('up', 'issues', () => {
this.selectPreviousIssue();
return false;
});
key('down', 'issues', () => {
this.selectNextIssue();
return false;
});
key('right', 'issues', () => {
this.openSelectedIssue();
return false;
});
key('left', 'issues', () => {
this.closeIssue();
return false;
});
}

detachShortcuts() {
key.deleteScope('issues');
}

getSelectedIndex(): ?number {
const { issues, selected } = this.state;
const index = issues.findIndex(issue => issue.key === selected);
return index !== -1 ? index : null;
}

selectNextIssue = () => {
const { issues } = this.state;
const selectedIndex = this.getSelectedIndex();
if (issues != null && selectedIndex != null && selectedIndex < issues.length - 1) {
if (getOpen(this.props.location.query)) {
this.openIssue(issues[selectedIndex + 1].key);
} else {
this.setState({ selected: issues[selectedIndex + 1].key });
}
}
};

selectPreviousIssue = () => {
const { issues } = this.state;
const selectedIndex = this.getSelectedIndex();
if (issues != null && selectedIndex != null && selectedIndex > 0) {
if (getOpen(this.props.location.query)) {
this.openIssue(issues[selectedIndex - 1].key);
} else {
this.setState({ selected: issues[selectedIndex - 1].key });
}
}
};

openIssue = (issue: string) => {
const path = {
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: issue
}
};
const open = getOpen(this.props.location.query);
if (open) {
this.props.router.replace(path);
} else {
this.props.router.push(path);
}
};

closeIssue = () => {
if (this.state.query) {
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: undefined
}
});
}
};

openSelectedIssue = () => {
const { selected } = this.state;
if (selected) {
this.openIssue(selected);
}
};

scrollToSelectedIssue = () => {
const { selected } = this.state;
if (selected) {
const element = document.querySelector(`[data-issue="${selected}"]`);
if (element) {
scrollToElement(element, 150, 100);
}
}
};

fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => {
const { component } = this.props;
const { myIssues, query } = this.state;

const parameters = {
componentKeys: component && component.key,
...serializeQuery(query),
s: 'FILE_LINE',
ps: 25,
facets: requestFacets
? [
'assignees',
'authors',
'createdAt',
'directories',
'fileUuids',
'languages',
'moduleUuids',
'projectUuids',
'resolutions',
'rules',
'severities',
'statuses',
'tags',
'types'
].join()
: undefined,
...additional
};

if (myIssues) {
Object.assign(parameters, { assignees: '__me__' });
}

return this.props.fetchIssues(parameters);
};

fetchFirstIssues() {
this.setState({ loading: true });
this.fetchIssues({}, true).then(({ facets, issues, paging, ...other }) => {
if (this.mounted) {
const open = getOpen(this.props.location.query);
this.setState({
facets: parseFacets(facets),
loading: false,
issues,
paging,
referencedComponents: keyBy(other.components, 'uuid'),
referencedLanguages: keyBy(other.languages, 'key'),
referencedRules: keyBy(other.rules, 'key'),
referencedUsers: keyBy(other.users, 'login'),
selected: issues.length > 0
? issues.find(issue => issue.key === open) != null ? open : issues[0].key
: undefined
});
}
});
}

fetchIssuesPage = (p: number): Promise<*> => {
return this.fetchIssues({ p });
};

fetchIssuesUntil = (p: number, done: (Array<Issue>, Paging) => boolean) => {
return this.fetchIssuesPage(p).then(response => {
const { issues, paging } = response;

return done(issues, paging)
? { issues, paging }
: this.fetchIssuesUntil(p + 1, done).then(nextResponse => {
return {
issues: [...issues, ...nextResponse.issues],
paging: nextResponse.paging
};
});
});
};

fetchMoreIssues = () => {
const { paging } = this.state;

if (!paging) {
return;
}

const p = paging.pageIndex + 1;

this.setState({ loading: true });
this.fetchIssuesPage(p).then(response => {
if (this.mounted) {
this.setState(state => ({
loading: false,
issues: [...state.issues, ...response.issues],
paging: response.paging
}));
}
});
};

fetchIssuesForComponent = (): Promise<Array<Issue>> => {
const { issues, paging } = this.state;

const open = getOpen(this.props.location.query);
const openIssue = issues.find(issue => issue.key === open);

if (!openIssue || !paging) {
return Promise.reject();
}

const isSameComponent = (issue: Issue): boolean => issue.component === openIssue.component;

const done = (issues: Array<Issue>, paging: Paging): boolean =>
paging.total <= paging.pageIndex * paging.pageSize ||
issues[issues.length - 1].component !== openIssue.component;

if (done(issues, paging)) {
return Promise.resolve(issues.filter(isSameComponent));
}

this.setState({ loading: true });
return this.fetchIssuesUntil(paging.pageIndex + 1, done).then(response => {
const nextIssues = [...issues, ...response.issues];

this.setState({
issues: nextIssues,
loading: false,
paging: response.paging
});
return nextIssues.filter(isSameComponent);
});
};

isFiltered = () => {
const serialized = serializeQuery(this.state.query);
return !areQueriesEqual(serialized, DEFAULT_QUERY);
};

getCheckedIssues = () => {
const issues = this.state.checked.map(checked =>
this.state.issues.find(issue => issue.key === checked));
const paging = { pageIndex: 1, pageSize: issues.length, total: issues.length };
return Promise.resolve({ issues, paging });
};

handleFilterChange = (changes: {}) => {
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, ...changes }),
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined
}
});
};

handleMyIssuesChange = (myIssues: boolean) => {
this.closeFacet('assignees');
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
id: this.props.component && this.props.component.key,
myIssues: myIssues ? 'true' : undefined
}
});
};

closeFacet = (property: string) => {
this.setState(state => ({
openFacets: { ...state.openFacets, [property]: false }
}));
};

handleFacetToggle = (property: string) => {
this.setState(state => ({
openFacets: { ...state.openFacets, [property]: !state.openFacets[property] }
}));
};

handleReset = () => {
this.props.router.push({
pathname: this.props.location.pathname,
query: {
...DEFAULT_QUERY,
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined
}
});
};

handleIssueCheck = (issue: string) => {
this.setState(state => ({
checked: state.checked.includes(issue)
? without(state.checked, issue)
: [...state.checked, issue]
}));
};

handleIssueChange = (issue: Issue) => {
this.setState(state => ({
issues: state.issues.map(candidate => candidate.key === issue.key ? issue : candidate)
}));
};

openBulkChange = (mode: 'all' | 'selected') => {
this.setState({ bulkChange: mode });
key.setScope('issues-bulk-change');
};

closeBulkChange = () => {
key.setScope('issues');
this.setState({ bulkChange: null });
};

handleBulkChangeClick = (e: Event & { target: HTMLElement }) => {
e.preventDefault();
e.target.blur();
this.openBulkChange('all');
};

handleBulkChangeSelectedClick = (e: Event & { target: HTMLElement }) => {
e.preventDefault();
e.target.blur();
this.openBulkChange('selected');
};

handleBulkChangeDone = () => {
this.fetchFirstIssues();
this.closeBulkChange();
};

renderBulkChange(openIssue?: Issue) {
const { component, currentUser } = this.props;
const { bulkChange, checked, paging } = this.state;

if (!currentUser.isLoggedIn || openIssue != null) {
return null;
}

return (
<div className="pull-left">
{checked.length > 0
? <div className="dropdown">
<button id="issues-bulk-change" data-toggle="dropdown">
{translate('bulk_change')}
<i className="icon-dropdown little-spacer-left" />
</button>
<ul className="dropdown-menu">
<li>
<a href="#" onClick={this.handleBulkChangeClick}>
{translateWithParameters('issues.bulk_change', paging ? paging.total : 0)}
</a>
</li>
<li>
<a href="#" onClick={this.handleBulkChangeSelectedClick}>
{translateWithParameters('issues.bulk_change_selected', checked.length)}
</a>
</li>
</ul>
</div>
: <button id="issues-bulk-change" onClick={this.handleBulkChangeClick}>
{translate('bulk_change')}
</button>}
{bulkChange != null &&
<BulkChangeModal
component={component}
currentUser={currentUser}
fetchIssues={bulkChange === 'all' ? this.fetchIssues : this.getCheckedIssues}
onClose={this.closeBulkChange}
onDone={this.handleBulkChangeDone}
onRequestFail={this.props.onRequestFail}
/>}
</div>
);
}

renderList(openIssue?: Issue) {
const { component, currentUser } = this.props;
const { issues, paging } = this.state;
const selectedIndex = this.getSelectedIndex();
const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null;

if (paging == null) {
return null;
}

return (
<div className={openIssue != null ? 'hidden' : undefined}>
{paging.total > 0 &&
<IssuesList
checked={this.state.checked}
component={component}
issues={issues}
onFilterChange={this.handleFilterChange}
onIssueChange={this.handleIssueChange}
onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined}
onIssueClick={this.openIssue}
selectedIssue={selectedIssue}
/>}

{paging.total > 0 &&
<ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />}

{paging.total === 0 && <EmptySearch />}
</div>
);
}

render() {
const { component, currentUser } = this.props;
const { issues, paging, query } = this.state;

const open = getOpen(this.props.location.query);
const openIssue = issues.find(issue => issue.key === open);

const selectedIndex = this.getSelectedIndex();

const top = component ? 95 : 30;

return (
<Page className="issues" id="issues-page">
<Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" />

<PageSide top={top}>
<PageFilters>
{currentUser.isLoggedIn &&
<MyIssuesFilter
myIssues={this.state.myIssues}
onMyIssuesChange={this.handleMyIssuesChange}
/>}
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
<Sidebar
component={component}
facets={this.state.facets}
myIssues={this.state.myIssues}
onFacetToggle={this.handleFacetToggle}
onFilterChange={this.handleFilterChange}
openFacets={this.state.openFacets}
query={query}
referencedComponents={this.state.referencedComponents}
referencedLanguages={this.state.referencedLanguages}
referencedRules={this.state.referencedRules}
referencedUsers={this.state.referencedUsers}
/>
</PageFilters>
</PageSide>

<PageMain>
<HeaderPanel border={true} top={top}>
<PageMainInner>
{this.renderBulkChange(openIssue)}
{openIssue != null &&
<div className="pull-left">
<ComponentBreadcrumbs component={component} issue={openIssue} />
</div>}
<PageActions
loading={this.state.loading}
openIssue={openIssue}
paging={paging}
selectedIndex={selectedIndex}
/>
</PageMainInner>
</HeaderPanel>

<PageMainInner>
<div>
{openIssue != null &&
<IssuesSourceViewer
openIssue={openIssue}
loadIssues={this.fetchIssuesForComponent}
onIssueChange={this.handleIssueChange}
onIssueSelect={this.openIssue}
/>}

{this.renderList(openIssue)}
</div>
</PageMainInner>
</PageMain>
</Page>
);
}
}

server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js → server/sonar-web/src/main/js/apps/issues/components/AppContainer.js View File

@@ -17,36 +17,38 @@
* 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';
// @flow
import { connect } from 'react-redux';
import init from '../init';
import { withRouter } from 'react-router';
import type { Dispatch } from 'redux';
import App from './App';
import { onFail } from '../../../store/rootActions';
import { getComponent, getCurrentUser } from '../../../store/rootReducer';
import { searchIssues } from '../../../api/issues';
import { parseIssueFromResponse } from '../../../helpers/issues';

class ComponentIssuesAppContainer extends React.Component {
componentDidMount() {
this.stop = init(this.refs.container, this.props.component, this.props.currentUser);
}

componentWillUnmount() {
this.stop();
}

render() {
// placing container inside div is required,
// because when backbone.marionette's layout is destroyed,
// it also destroys the root element,
// but react wants it to be there to unmount it
return (
<div>
<div ref="container" />
</div>
);
}
}
type Query = { [string]: string };

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
component: ownProps.location.query.id
? getComponent(state, ownProps.location.query.id)
: undefined,
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(ComponentIssuesAppContainer);
const fetchIssues = (query: Query) =>
(dispatch: Dispatch<*>) =>
searchIssues({ ...query, additionalFields: '_all' }).then(
response => {
const parsedIssues = response.issues.map(issue =>
parseIssueFromResponse(issue, response.components, response.users, response.rules));
return { ...response, issues: parsedIssues };
},
onFail(dispatch)
);

const onRequestFail = (error: Error) => (dispatch: Dispatch<*>) => onFail(dispatch)(error);

const mapDispatchToProps = { fetchIssues, onRequestFail };

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));

+ 522
- 0
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js View File

@@ -0,0 +1,522 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import Modal from 'react-modal';
import Select from 'react-select';
import { css } from 'glamor';
import { pickBy, sortBy } from 'lodash';
import SearchSelect from './SearchSelect';
import Checkbox from '../../../components/controls/Checkbox';
import Tooltip from '../../../components/controls/Tooltip';
import MarkdownTips from '../../../components/common/MarkdownTips';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import Avatar from '../../../components/ui/Avatar';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { searchAssignees } from '../utils';
import type { Paging, Component, CurrentUser } from '../utils';
import type { Issue } from '../../../components/issue/types';

type Props = {|
component?: Component,
currentUser: CurrentUser,
fetchIssues: ({}) => Promise<*>,
onClose: () => void,
onDone: () => void,
onRequestFail: (Error) => void
|};

type State = {|
issues: Array<Issue>,
// used for initial loading of issues
loading: boolean,
paging?: Paging,
// used when submitting a form
submitting: boolean,
tags?: Array<string>,

// form fields
addTags?: Array<string>,
assignee?: string,
comment?: string,
notifications?: boolean,
removeTags?: Array<string>,
severity?: string,
transition?: string,
type?: string
|};

const hasAction = (action: string) =>
(issue: Issue): boolean => issue.actions && issue.actions.includes(action);

export default class BulkChangeModal extends React.PureComponent {
mounted: boolean;
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = { issues: [], loading: true, submitting: false };
}

componentDidMount() {
this.mounted = true;
Promise.all([this.loadIssues(), searchIssueTags()]).then(([issues, tags]) => {
if (this.mounted) {
this.setState({
issues: issues.issues,
loading: false,
paging: issues.paging,
tags
});
}
});
}

componentWillUnmount() {
this.mounted = false;
}

handleCloseClick = (e: Event & { target: HTMLElement }) => {
e.preventDefault();
e.target.blur();
this.props.onClose();
};

loadIssues = () => {
return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 });
};

handleAssigneeSearch = (query: string) => {
if (query.length > 1) {
return searchAssignees(query, this.props.component);
} else {
const { currentUser } = this.props;
const { issues } = this.state;
const options = [];

if (currentUser.isLoggedIn) {
const canBeAssignedToMe = issues.filter(
issue => issue.assignee !== currentUser.login
).length > 0;
if (canBeAssignedToMe) {
options.push({
email: currentUser.email,
label: currentUser.name,
value: currentUser.login
});
}
}

const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0;
if (canBeUnassigned) {
options.push({ label: translate('unassigned'), value: '' });
}

return Promise.resolve(options);
}
};

handleAssigneeSelect = (assignee: string) => {
this.setState({ assignee });
};

handleFieldCheck = (field: string) =>
(checked: boolean) => {
if (!checked) {
this.setState({ [field]: undefined });
} else if (field === 'notifications') {
this.setState({ [field]: true });
}
};

handleFieldChange = (field: string) =>
(event: { target: HTMLInputElement }) => {
this.setState({ [field]: event.target.value });
};

handleSelectFieldChange = (field: string) =>
({ value }: { value: string }) => {
this.setState({ [field]: value });
};

handleMultiSelectFieldChange = (field: string) =>
(options: Array<{ value: string }>) => {
this.setState({ [field]: options.map(option => option.value) });
};

handleSubmit = (e: Event) => {
e.preventDefault();
const query = pickBy({
assign: this.state.assignee,
set_type: this.state.type,
set_severity: this.state.severity,
add_tags: this.state.addTags && this.state.addTags.join(),
remove_tags: this.state.removeTags && this.state.removeTags.join(),
do_transition: this.state.transition,
comment: this.state.comment,
sendNotifications: this.state.notifications
});
const issueKeys = this.state.issues.map(issue => issue.key);

this.setState({ submitting: true });
bulkChangeIssues(issueKeys, query).then(
() => {
this.setState({ submitting: false });
this.props.onDone();
},
(error: Error) => {
this.setState({ submitting: false });
this.props.onRequestFail(error);
}
);
};

getAvailableTransitions(issues: Array<Issue>): Array<{ transition: string, count: number }> {
const transitions = {};
issues.forEach(issue => {
if (issue.transitions) {
issue.transitions.forEach(t => {
if (transitions[t] != null) {
transitions[t]++;
} else {
transitions[t] = 1;
}
});
}
});
return sortBy(Object.keys(transitions)).map(transition => ({
transition,
count: transitions[transition]
}));
}

renderCancelButton = () => (
<a id="bulk-change-cancel" href="#" onClick={this.handleCloseClick}>
{translate('cancel')}
</a>
);

renderLoading = () => (
<div>
<div className="modal-head">
<h2>{translate('bulk_change')}</h2>
</div>
<div className="modal-body">
<div className="text-center">
<i className="spinner spinner-margin" />
</div>
</div>
<div className="modal-foot">
{this.renderCancelButton()}
</div>
</div>
);

renderCheckbox = (field: string) => (
<Checkbox
className={css({ paddingTop: 6, paddingRight: 8 })}
checked={this.state[field] != null}
onCheck={this.handleFieldCheck(field)}
/>
);

renderAffected = (affected: number) => (
<div className="pull-right note">
({translateWithParameters('issue_bulk_change.x_issues', affected)})
</div>
);

renderField = (field: string, label: string, affected: ?number, input: Object) => (
<div className="modal-field" id={`issues-bulk-change-${field}`}>
<label htmlFor={field}>{translate(label)}</label>
{this.renderCheckbox(field)}
{input}
{affected != null && this.renderAffected(affected)}
</div>
);

renderAssigneeOption = (option: { avatar?: string, email?: string, label: string }) => (
<span>
{(option.avatar != null || option.email != null) &&
<Avatar
className="little-spacer-right"
email={option.email}
hash={option.avatar}
size={16}
/>}
{option.label}
</span>
);

renderAssigneeField = () => {
const affected: number = this.state.issues.filter(hasAction('assign')).length;

if (affected === 0) {
return null;
}

const input = (
<SearchSelect
onSearch={this.handleAssigneeSearch}
onSelect={this.handleAssigneeSelect}
minimumQueryLength={0}
renderOption={this.renderAssigneeOption}
resetOnBlur={false}
value={this.state.assignee}
/>
);

return this.renderField('assignee', 'issue.assign.formlink', affected, input);
};

renderTypeField = () => {
const affected: number = this.state.issues.filter(hasAction('set_type')).length;

if (affected === 0) {
return null;
}

const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
const options = types.map(type => ({ label: translate('issue.type', type), value: type }));

const optionRenderer = (option: { label: string, value: string }) => (
<span>
<IssueTypeIcon className="little-spacer-right" query={option.value} />
{option.label}
</span>
);

const input = (
<Select
clearable={false}
id="type"
onChange={this.handleSelectFieldChange('type')}
options={options}
optionRenderer={optionRenderer}
searchable={false}
value={this.state.type}
valueRenderer={optionRenderer}
/>
);

return this.renderField('type', 'issue.set_type', affected, input);
};

renderSeverityField = () => {
const affected: number = this.state.issues.filter(hasAction('set_severity')).length;

if (affected === 0) {
return null;
}

const severities = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
const options = severities.map(severity => ({
label: translate('severity', severity),
value: severity
}));

const input = (
<Select
clearable={false}
id="severity"
onChange={this.handleSelectFieldChange('severity')}
options={options}
optionRenderer={option => <SeverityHelper severity={option.value} />}
searchable={false}
value={this.state.severity}
valueRenderer={option => <SeverityHelper severity={option.value} />}
/>
);

return this.renderField('severity', 'issue.set_severity', affected, input);
};

renderAddTagsField = () => {
const affected: number = this.state.issues.filter(hasAction('set_tags')).length;

if (this.state.tags == null || affected === 0) {
return null;
}

const options = this.state.tags.map(tag => ({ label: tag, value: tag }));

const input = (
<Select
clearable={false}
id="add_tags"
multi={true}
onChange={this.handleMultiSelectFieldChange('addTags')}
options={options}
searchable={true}
value={this.state.addTags}
/>
);

return this.renderField('addTags', 'issue.add_tags', affected, input);
};

renderRemoveTagsField = () => {
const affected: number = this.state.issues.filter(hasAction('set_tags')).length;

if (this.state.tags == null || affected === 0) {
return null;
}

const options = this.state.tags.map(tag => ({ label: tag, value: tag }));

const input = (
<Select
clearable={false}
id="remove_tags"
multi={true}
onChange={this.handleMultiSelectFieldChange('removeTags')}
options={options}
searchable={true}
value={this.state.removeTags}
/>
);

return this.renderField('removeTags', 'issue.remove_tags', affected, input);
};

renderTransitionsField = () => {
const transitions = this.getAvailableTransitions(this.state.issues);

if (transitions.length === 0) {
return null;
}

return (
<div className="modal-field">
<label>{translate('issue.transition')}</label>
{transitions.map(transition => (
<span key={transition.transition}>
<input
checked={this.state.transition === transition.transition}
id={`transition-${transition.transition}`}
name="do_transition.transition"
onChange={this.handleFieldChange('transition')}
type="radio"
value={transition.transition}
/>
<label
htmlFor={`transition-${transition.transition}`}
style={{ float: 'none', display: 'inline', left: 0, cursor: 'pointer' }}>
{translate('issue.transition', transition.transition)}
</label>
{this.renderAffected(transition.count)}
<br />
</span>
))}
</div>
);
};

renderCommentField = () => {
const affected: number = this.state.issues.filter(hasAction('comment')).length;

if (affected === 0) {
return null;
}

return (
<div className="modal-field">
<label htmlFor="comment">
{translate('issue.comment.formlink')}
<Tooltip overlay={translate('issue_bulk_change.comment.help')}>
<i className="icon-help little-spacer-left" />
</Tooltip>
</label>
<div>
<textarea
id="comment"
onChange={this.handleFieldChange('comment')}
rows="4"
style={{ width: '100%' }}
value={this.state.comment || ''}
/>
</div>
<div className="pull-right">
<MarkdownTips />
</div>
</div>
);
};

renderNotificationsField = () => (
<div className="modal-field">
<label htmlFor="send-notifications">{translate('issue.send_notifications')}</label>
{this.renderCheckbox('notifications')}
</div>
);

renderForm = () => {
const { issues, paging, submitting } = this.state;

const limitReached: boolean = paging != null &&
paging.total > paging.pageIndex * paging.pageSize;

return (
<form id="bulk-change-form" onSubmit={this.handleSubmit}>
<div className="modal-head">
<h2>{translateWithParameters('issue_bulk_change.form.title', issues.length)}</h2>
</div>

<div className="modal-body">
{limitReached &&
<div className="alert alert-warning">
{translateWithParameters('issue_bulk_change.max_issues_reached', issues.length)}
</div>}

{this.renderAssigneeField()}
{this.renderTypeField()}
{this.renderSeverityField()}
{this.renderAddTagsField()}
{this.renderRemoveTagsField()}
{this.renderTransitionsField()}
{this.renderCommentField()}
{this.renderNotificationsField()}
</div>

<div className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
<button disabled={submitting} id="bulk-change-submit">{translate('apply')}</button>
{this.renderCancelButton()}
</div>
</form>
);
};

render() {
return (
<Modal
isOpen={true}
contentLabel="modal"
className="modal"
overlayClassName="modal-overlay"
onRequestClose={this.props.onClose}>
{this.state.loading ? this.renderLoading() : this.renderForm()}
</Modal>
);
}
}

+ 72
- 0
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js View File

@@ -0,0 +1,72 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { Link } from 'react-router';
import Organization from '../../../components/shared/Organization';
import { collapsePath, limitComponentName } from '../../../helpers/path';
import { getProjectUrl } from '../../../helpers/urls';
import type { Component } from '../utils';

type Props = {
component?: Component,
issue: Object
};

export default class ComponentBreadcrumbs extends React.PureComponent {
props: Props;

render() {
const { component, issue } = this.props;

const displayOrganization = component == null || ['VW', 'SVW'].includes(component.qualifier);
const displayProject = component == null ||
!['TRK', 'BRC', 'DIR'].includes(component.qualifier);
const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier);

return (
<div className="component-name">
{displayOrganization &&
<Organization linkClassName="link-no-underline" organizationKey={issue.organization} />}

{displayProject &&
<span>
<Link to={getProjectUrl(issue.project)} className="link-no-underline">
{limitComponentName(issue.projectName)}
</Link>
<span className="slash-separator" />
</span>}

{displaySubProject &&
issue.subProject != null &&
<span>
<Link to={getProjectUrl(issue.subProject)} className="link-no-underline">
{limitComponentName(issue.subProjectName)}
</Link>
<span className="slash-separator" />
</span>}

<Link to={getProjectUrl(issue.component)} className="link-no-underline">
{collapsePath(issue.componentLongName)}
</Link>
</div>
);
}
}

server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js → server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js View File

@@ -17,39 +17,39 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { connect } from 'react-redux';
import init from '../init';
import { getCurrentUser } from '../../../store/rootReducer';
import { css } from 'glamor';
import { translate } from '../../../helpers/l10n';

class IssuesAppContainer extends React.Component {
static propTypes = {
currentUser: React.PropTypes.any.isRequired
};
type Props = {
displayReset: boolean,
onReset: () => void
};

componentDidMount() {
this.stop = init(this.refs.container, this.props.currentUser);
}
const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' });

componentWillUnmount() {
this.stop();
}
export default class FiltersHeader extends React.PureComponent {
props: Props;

handleResetClick = (e: Event & { currentTarget: HTMLElement }) => {
e.preventDefault();
e.currentTarget.blur();
this.props.onReset();
};

render() {
// placing container inside div is required,
// because when backbone.marionette's layout is destroyed,
// it also destroys the root element,
// but react wants it to be there to unmount it
return (
<div>
<div ref="container" />
<div className={styles}>
{this.props.displayReset &&
<div className={css({ float: 'right' })}>
<button className="button-red" onClick={this.handleResetClick}>
{translate('clear_all_filters')}
</button>
</div>}

<h3>{translate('filters')}</h3>
</div>
);
}
}

const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});

export default connect(mapStateToProps)(IssuesAppContainer);

+ 106
- 0
server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js View File

@@ -0,0 +1,106 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css, media } from 'glamor';
import { clearfix } from 'glamor/utils';
import { throttle } from 'lodash';

type Props = {|
border: boolean,
children?: React.Element<*>,
top?: number
|};

type State = {
scrolled: boolean
};

export default class HeaderPanel extends React.PureComponent {
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = { scrolled: this.isScrolled() };
this.handleScroll = throttle(this.handleScroll, 50);
}

componentDidMount() {
if (this.props.top != null) {
window.addEventListener('scroll', this.handleScroll);
}
}

componentWillUnmount() {
if (this.props.top != null) {
window.removeEventListener('scroll', this.handleScroll);
}
}

isScrolled = () => window.scrollY > 10;

handleScroll = () => {
this.setState({ scrolled: this.isScrolled() });
};

render() {
const commonStyles = {
height: 56,
lineHeight: '24px',
padding: '16px 20px',
boxSizing: 'border-box',
borderBottom: this.props.border ? '1px solid #e6e6e6' : undefined,
backgroundColor: '#f3f3f3'
};

const inner = this.props.top
? <div
className={css(
commonStyles,
{
position: 'fixed',
zIndex: 30,
top: this.props.top,
left: 'calc(50vw - 360px + 1px)',
right: 0,
boxShadow: this.state.scrolled ? '0 2px 4px rgba(0, 0, 0, .125)' : 'none',
transition: 'box-shadow 0.3s ease'
},
media('(max-width: 1320px)', { left: 301 })
)}>
{this.props.children}
</div>
: this.props.children;

return (
<div
className={css(clearfix(), commonStyles, {
marginTop: -20,
marginBottom: 20,
marginLeft: -20,
marginRight: -20,
'& .component-name': { lineHeight: '24px' }
})}>
{inner}
</div>
);
}
}

+ 62
- 0
server/sonar-web/src/main/js/apps/issues/components/IssuesList.js View File

@@ -0,0 +1,62 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import ListItem from './ListItem';
import type { Issue } from '../../../components/issue/types';
import type { Component } from '../utils';

type Props = {|
checked: Array<string>,
component?: Component,
issues: Array<Issue>,
onFilterChange: (changes: {}) => void,
onIssueChange: (Issue) => void,
onIssueCheck?: (string) => void,
onIssueClick: (string) => void,
selectedIssue: ?Issue
|};

export default class IssuesList extends React.PureComponent {
props: Props;

render() {
const { checked, component, issues, selectedIssue } = this.props;

return (
<div>
{issues.map((issue, index) => (
<ListItem
checked={checked.includes(issue.key)}
component={component}
key={issue.key}
issue={issue}
onChange={this.props.onIssueChange}
onCheck={this.props.onIssueCheck}
onClick={this.props.onIssueClick}
onFilterChange={this.props.onFilterChange}
previousIssue={index > 0 ? issues[index - 1] : null}
selected={selectedIssue != null && selectedIssue.key === issue.key}
/>
))}
</div>
);
}
}

+ 68
- 0
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js View File

@@ -0,0 +1,68 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { scrollToElement } from '../../../helpers/scrolling';
import type { Issue } from '../../../components/issue/types';

type Props = {|
loadIssues: () => Promise<*>,
onIssueChange: (Issue) => void,
onIssueSelect: (string) => void,
openIssue: Issue
|};

export default class IssuesSourceViewer extends React.PureComponent {
node: HTMLElement;
props: Props;

componentDidUpdate(prevProps: Props) {
if (prevProps.openIssue.component === this.props.openIssue.component) {
this.scrollToIssue();
}
}

scrollToIssue = () => {
const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`);
if (element) {
scrollToElement(element, 100, 100);
}
};

render() {
const { openIssue } = this.props;

return (
<div ref={node => this.node = node}>
<SourceViewer
aroundLine={openIssue.line}
component={openIssue.component}
displayAllIssues={true}
loadIssues={this.props.loadIssues}
onLoaded={this.scrollToIssue}
onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
selectedIssue={openIssue.key}
/>
</div>
);
}
}

+ 106
- 0
server/sonar-web/src/main/js/apps/issues/components/ListItem.js View File

@@ -0,0 +1,106 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import ComponentBreadcrumbs from './ComponentBreadcrumbs';
import Issue from '../../../components/issue/Issue';
import type { Issue as IssueType } from '../../../components/issue/types';
import type { Component } from '../utils';

type Props = {|
checked: boolean,
component?: Component,
issue: IssueType,
onChange: (IssueType) => void,
onCheck?: (string) => void,
onClick: (string) => void,
onFilterChange: (changes: {}) => void,
previousIssue: ?Object,
selected: boolean
|};

type State = {
similarIssues: boolean
};

export default class ListItem extends React.PureComponent {
props: Props;
state: State = { similarIssues: false };

handleFilter = (property: string, issue: IssueType) => {
const { onFilterChange } = this.props;

const issuesReset = { issues: [] };

if (property.startsWith('tag###')) {
const tag = property.substr(6);
return onFilterChange({ ...issuesReset, tags: [tag] });
}

switch (property) {
case 'type':
return onFilterChange({ ...issuesReset, types: [issue.type] });
case 'severity':
return onFilterChange({ ...issuesReset, severities: [issue.severity] });
case 'status':
return onFilterChange({ ...issuesReset, statuses: [issue.status] });
case 'resolution':
return issue.resolution != null
? onFilterChange({ ...issuesReset, resolved: true, resolutions: [issue.resolution] })
: onFilterChange({ ...issuesReset, resolved: false, resolutions: [] });
case 'assignee':
return issue.assignee != null
? onFilterChange({ ...issuesReset, assigned: true, assignees: [issue.assignee] })
: onFilterChange({ ...issuesReset, assigned: false, assignees: [] });
case 'rule':
return onFilterChange({ ...issuesReset, rules: [issue.rule] });
case 'project':
return onFilterChange({ ...issuesReset, projects: [issue.projectUuid] });
case 'module':
return onFilterChange({ ...issuesReset, modules: [issue.subProjectUuid] });
case 'file':
return onFilterChange({ ...issuesReset, files: [issue.componentUuid] });
}
};

render() {
const { component, issue, previousIssue } = this.props;

const displayComponent = previousIssue == null || previousIssue.component !== issue.component;

return (
<div className="issues-workspace-list-item">
{displayComponent &&
<div className="issues-workspace-list-component">
<ComponentBreadcrumbs component={component} issue={this.props.issue} />
</div>}
<Issue
checked={this.props.checked}
issue={issue}
onChange={this.props.onChange}
onCheck={this.props.onCheck}
onClick={this.props.onClick}
onFilter={this.handleFilter}
selected={this.props.selected}
/>
</div>
);
}
}

+ 60
- 0
server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js View File

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css } from 'glamor';
import { translate } from '../../../helpers/l10n';

type Props = {|
myIssues: boolean,
onMyIssuesChange: (boolean) => void
|};

export default class MyIssuesFilter extends React.Component {
props: Props;

handleClick = (myIssues: boolean) =>
(e: Event & { currentTarget: HTMLElement }) => {
e.preventDefault();
e.currentTarget.blur();
this.props.onMyIssuesChange(myIssues);
};

render() {
const { myIssues } = this.props;

return (
<div className={css({ marginBottom: 24, textAlign: 'center' })}>
<div className="button-group">
<button
className={myIssues ? 'button-active' : undefined}
onClick={this.handleClick(true)}>
{translate('issues.my_issues')}
</button>
<button
className={myIssues ? undefined : 'button-active'}
onClick={this.handleClick(false)}>
{translate('all')}
</button>
</div>
</div>
);
}
}

+ 77
- 0
server/sonar-web/src/main/js/apps/issues/components/PageActions.js View File

@@ -0,0 +1,77 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { css } from 'glamor';
import type { Paging } from '../utils';
import { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';

type Props = {|
loading: boolean,
openIssue: ?{},
paging: ?Paging,
selectedIndex: ?number
|};

export default class PageActions extends React.Component {
props: Props;

renderShortcuts() {
return (
<span className="note big-spacer-right">
<span className="big-spacer-right">
<span className="shortcut-button little-spacer-right">↑</span>
<span className="shortcut-button little-spacer-right">↓</span>
{translate('issues.to_select_issues')}
</span>

<span>
<span className="shortcut-button little-spacer-right">←</span>
<span className="shortcut-button little-spacer-right">→</span>
{translate('issues.to_navigate')}
</span>
</span>
);
}

render() {
const { openIssue, paging, selectedIndex } = this.props;

return (
<div className={css({ float: 'right' })}>
{openIssue == null && this.renderShortcuts()}

<div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}>
{this.props.loading && <i className="spinner spacer-right" />}
{paging != null &&
<span>
<strong>
{selectedIndex != null && <span>{selectedIndex + 1} / </span>}
{formatMeasure(paging.total, 'INT')}
</strong>
{' '}
{translate('issues.issues')}
</span>}
</div>
</div>
);
}
}

+ 122
- 0
server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js View File

@@ -0,0 +1,122 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import Select from 'react-select';
import { debounce } from 'lodash';
import { translate, translateWithParameters } from '../../../helpers/l10n';

type Option = { label: string, value: string };

type Props = {|
minimumQueryLength: number,
onSearch: (query: string) => Promise<Array<Option>>,
onSelect: (value: string) => void,
renderOption?: (option: Object) => React.Element<*>,
resetOnBlur: boolean,
value?: string
|};

type State = {
loading: boolean,
options: Array<Option>,
query: string
};

export default class SearchSelect extends React.PureComponent {
mounted: boolean;
props: Props;
state: State;

static defaultProps = {
minimumQueryLength: 2,
resetOnBlur: true
};

constructor(props: Props) {
super(props);
this.state = { loading: false, options: [], query: '' };
this.search = debounce(this.search, 250);
}

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

search = (query: string) => {
this.props.onSearch(query).then(options => {
if (this.mounted) {
this.setState({ loading: false, options });
}
});
};

handleBlur = () => {
this.setState({ options: [], query: '' });
};

handleChange = (option: Option) => {
this.props.onSelect(option.value);
};

handleInputChange = (query: string = '') => {
if (query.length >= this.props.minimumQueryLength) {
this.setState({ loading: true, query });
this.search(query);
} else {
this.setState({ options: [], query });
}
};

// disable internal filtering
handleFilterOption = () => true;

render() {
return (
<Select
autofocus={true}
cache={false}
className="input-super-large"
clearable={false}
filterOption={this.handleFilterOption}
isLoading={this.state.loading}
noResultsText={
this.state.query.length < this.props.minimumQueryLength
? translateWithParameters('select2.tooShort', this.props.minimumQueryLength)
: translate('select2.noMatches')
}
onBlur={this.props.resetOnBlur ? this.handleBlur : undefined}
onChange={this.handleChange}
onInputChange={this.handleInputChange}
onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined}
optionRenderer={this.props.renderOption}
options={this.state.options}
placeholder={translate('search_verb')}
searchable={true}
value={this.props.value}
valueRenderer={this.props.renderOption}
/>
);
}
}

+ 49
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js View File

@@ -0,0 +1,49 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { shallow } from 'enzyme';
import SearchSelect from '../SearchSelect';

jest.mock('lodash', () => ({
debounce: fn => fn
}));

it('should render Select', () => {
expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
});

it('should call onSelect', () => {
const onSelect = jest.fn();
const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
wrapper.prop('onChange')({ value: 'foo' });
expect(onSelect).lastCalledWith('foo');
});

it('should call onSearch', () => {
const onSearch = jest.fn().mockReturnValue(Promise.resolve([]));
const wrapper = shallow(
<SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} />
);
wrapper.prop('onInputChange')('f');
expect(onSearch).not.toHaveBeenCalled();
wrapper.prop('onInputChange')('foo');
expect(onSearch).lastCalledWith('foo');
});

+ 48
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap View File

@@ -0,0 +1,48 @@
exports[`test should render Select 1`] = `
<Select
addLabelText="Add \"{label}\"?"
arrowRenderer={[Function]}
autofocus={true}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
cache={false}
className="input-super-large"
clearAllText="Clear all"
clearValueText="Clear value"
clearable={false}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOption={[Function]}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={Object {}}
isLoading={false}
joinValues={false}
labelKey="label"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={false}
noResultsText="select2.tooShort.2"
onBlur={[Function]}
onBlurResetsInput={true}
onChange={[Function]}
onCloseResetsInput={true}
onInputChange={[Function]}
openAfterFocus={false}
optionComponent={[Function]}
options={Array []}
pageSize={5}
placeholder="search_verb"
required={false}
scrollMenuIntoView={true}
searchable={true}
simpleValue={false}
tabSelectsValue={true}
valueComponent={[Function]}
valueKey="value" />
`;

+ 0
- 217
server/sonar-web/src/main/js/apps/issues/controller.js View File

@@ -1,217 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import Backbone from 'backbone';
import Controller from '../../components/navigator/controller';
import ComponentViewer from './component-viewer/main';
import getStore from '../../app/utils/getStore';
import { receiveIssues } from '../../store/issues/duck';

const FACET_DATA_FIELDS = ['components', 'users', 'rules', 'languages'];

export default Controller.extend({
_issuesParameters() {
return {
p: this.options.app.state.get('page'),
ps: this.pageSize,
asc: true,
additionalFields: '_all',
facets: this._facetsFromServer().join()
};
},

receiveIssues(issues) {
const store = getStore();
store.dispatch(receiveIssues(issues));
},

fetchList(firstPage) {
const that = this;
if (firstPage == null) {
firstPage = true;
}
if (firstPage) {
this.options.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true });
this.closeComponentViewer();
}
const data = this._issuesParameters();
Object.assign(data, this.options.app.state.get('query'));
if (this.options.app.state.get('query').assigned_to_me) {
Object.assign(data, { assignees: '__me__' });
}
if (this.options.app.state.get('isContext')) {
Object.assign(data, this.options.app.state.get('contextQuery'));
}
return $.get(window.baseUrl + '/api/issues/search', data).done(r => {
const issues = that.options.app.list.parseIssues(r);
this.receiveIssues(issues);
if (firstPage) {
const issues = that.options.app.list.parseIssues(r);
that.options.app.list.reset(issues);
} else {
const issues = that.options.app.list.parseIssues(r, that.options.app.list.length);
that.options.app.list.add(issues);
}
that.options.app.list.setIndex();
FACET_DATA_FIELDS.forEach(field => {
that.options.app.facets[field] = r[field];
});
that.options.app.facets.reset(that._allFacets());
that.options.app.facets.add(r.facets, { merge: true });
that.enableFacets(that._enabledFacets());
if (firstPage) {
that.options.app.state.set({
page: r.p,
pageSize: r.ps,
total: r.total,
maxResultsReached: r.p * r.ps >= r.total
});
} else {
that.options.app.state.set({
page: r.p,
maxResultsReached: r.p * r.ps >= r.total
});
}
if (firstPage && that.isIssuePermalink()) {
that.showComponentViewer(that.options.app.list.first());
}
});
},

isIssuePermalink() {
const query = this.options.app.state.get('query');
return query.issues != null && this.options.app.list.length === 1;
},

_mergeCollections(a, b) {
const collection = new Backbone.Collection(a);
collection.add(b, { merge: true });
return collection.toJSON();
},

requestFacet(id) {
const that = this;
const facet = this.options.app.facets.get(id);
const data = {
facets: id,
ps: 1,
additionalFields: '_all',
...this.options.app.state.get('query')
};
if (this.options.app.state.get('query').assigned_to_me) {
Object.assign(data, { assignees: '__me__' });
}
if (this.options.app.state.get('isContext')) {
Object.assign(data, this.options.app.state.get('contextQuery'));
}
return $.get(window.baseUrl + '/api/issues/search', data, r => {
FACET_DATA_FIELDS.forEach(field => {
that.options.app.facets[field] = that._mergeCollections(
that.options.app.facets[field],
r[field]
);
});
const facetData = r.facets.find(facet => facet.property === id);
if (facetData != null) {
facet.set(facetData);
}
});
},

newSearch() {
this.options.app.state.unset('filter');
return this.options.app.state.setQuery({ resolved: 'false' });
},

parseQuery() {
const q = Controller.prototype.parseQuery.apply(this, arguments);
delete q.asc;
delete q.s;
delete q.id;
return q;
},

getQueryAsObject() {
const state = this.options.app.state;
const query = state.get('query');
if (query.assigned_to_me) {
Object.assign(query, { assignees: '__me__' });
}
if (state.get('isContext')) {
Object.assign(query, state.get('contextQuery'));
}
return query;
},

getQuery(separator, addContext, handleMyIssues = false) {
if (separator == null) {
separator = '|';
}
if (addContext == null) {
addContext = false;
}
const filter = this.options.app.state.get('query');
if (addContext && this.options.app.state.get('isContext')) {
Object.assign(filter, this.options.app.state.get('contextQuery'));
}
if (handleMyIssues && this.options.app.state.get('query').assigned_to_me) {
Object.assign(filter, { assignees: '__me__' });
}
const route = [];
Object.keys(filter).forEach(property => {
route.push(`${property}=${encodeURIComponent(filter[property])}`);
});
return route.join(separator);
},

_prepareComponent(issue) {
return {
key: issue.get('component'),
name: issue.get('componentLongName'),
qualifier: issue.get('componentQualifier'),
subProject: issue.get('subProject'),
subProjectName: issue.get('subProjectLongName'),
project: issue.get('project'),
projectName: issue.get('projectLongName'),
projectOrganization: issue.get('projectOrganization')
};
},

showComponentViewer(issue) {
this.options.app.layout.workspaceComponentViewerRegion.reset();
key.setScope('componentViewer');
this.options.app.issuesView.unbindScrollEvents();
this.options.app.state.set('component', this._prepareComponent(issue));
this.options.app.componentViewer = new ComponentViewer({ app: this.options.app });
this.options.app.layout.workspaceComponentViewerRegion.show(this.options.app.componentViewer);
this.options.app.layout.showComponentViewer();
return this.options.app.componentViewer.openFileByIssue(issue);
},

closeComponentViewer() {
key.setScope('list');
$('body').click();
this.options.app.state.unset('component');
this.options.app.layout.workspaceComponentViewerRegion.reset();
this.options.app.layout.hideComponentViewer();
this.options.app.issuesView.bindScrollEvents();
return this.options.app.issuesView.scrollTo();
}
});

+ 0
- 63
server/sonar-web/src/main/js/apps/issues/facets-view.js View File

@@ -1,63 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import FacetsView from '../../components/navigator/facets-view';
import BaseFacet from './facets/base-facet';
import TypeFacet from './facets/type-facet';
import SeverityFacet from './facets/severity-facet';
import StatusFacet from './facets/status-facet';
import ProjectFacet from './facets/project-facet';
import ModuleFacet from './facets/module-facet';
import AssigneeFacet from './facets/assignee-facet';
import RuleFacet from './facets/rule-facet';
import TagFacet from './facets/tag-facet';
import ResolutionFacet from './facets/resolution-facet';
import CreationDateFacet from './facets/creation-date-facet';
import FileFacet from './facets/file-facet';
import LanguageFacet from './facets/language-facet';
import AuthorFacet from './facets/author-facet';
import IssueKeyFacet from './facets/issue-key-facet';
import ContextFacet from './facets/context-facet';
import ModeFacet from './facets/mode-facet';

const viewsMapping = {
types: TypeFacet,
severities: SeverityFacet,
statuses: StatusFacet,
assignees: AssigneeFacet,
resolutions: ResolutionFacet,
createdAt: CreationDateFacet,
projectUuids: ProjectFacet,
moduleUuids: ModuleFacet,
rules: RuleFacet,
tags: TagFacet,
fileUuids: FileFacet,
languages: LanguageFacet,
authors: AuthorFacet,
issues: IssueKeyFacet,
context: ContextFacet,
facetMode: ModeFacet
};

export default FacetsView.extend({
getChildView(model) {
const view = viewsMapping[model.get('property')];
return view ? view : BaseFacet;
}
});

+ 0
- 135
server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js View File

@@ -1,135 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import { sortBy } from 'lodash';
import CustomValuesFacet from './custom-values-facet';
import Template from '../templates/facets/issues-assignee-facet.hbs';

export default CustomValuesFacet.extend({
template: Template,

initialize() {
this.context = {
isContext: this.options.app.state.get('isContext'),
organization: this.options.app.state.get('contextOrganization')
};
},

getUrl() {
return window.baseUrl +
(this.context.isContext ? '/api/organizations/search_members' : '/api/users/search');
},

prepareAjaxSearch() {
return {
quietMillis: 300,
url: this.getUrl(),
data: (term, page) => {
if (this.context.isContext && this.context.organization) {
return { q: term, p: page, organization: this.context.organization };
} else {
return { q: term, p: page };
}
},
results: window.usersToSelect2
};
},

onRender() {
CustomValuesFacet.prototype.onRender.apply(this, arguments);

const myIssuesSelected = !!this.options.app.state.get('query').assigned_to_me;
this.$el.toggleClass('hidden', myIssuesSelected);

const value = this.options.app.state.get('query').assigned;
if (value != null && (!value || value === 'false')) {
this.$('.js-facet').filter('[data-unassigned]').addClass('active');
}
},

toggleFacet(e) {
const unassigned = $(e.currentTarget).is('[data-unassigned]');
$(e.currentTarget).toggleClass('active');
if (unassigned) {
const checked = $(e.currentTarget).is('.active');
const value = checked ? 'false' : null;
return this.options.app.state.updateFilter({
assigned: value,
assignees: null,
assigned_to_me: null
});
} else {
return this.options.app.state.updateFilter({
assigned: null,
assignees: this.getValue(),
assigned_to_me: null
});
}
},

getValuesWithLabels() {
const values = this.model.getValues();
const users = this.options.app.facets.users;
values.forEach(v => {
const login = v.val;
let name = '';
if (login) {
const user = users.find(user => user.login === login);
if (user != null) {
name = user.name;
}
}
v.label = name;
});
return values;
},

disable() {
return this.options.app.state.updateFilter({
assigned: null,
assignees: null
});
},

addCustomValue() {
const property = this.model.get('property');
const customValue = this.$('.js-custom-value').select2('val');
let value = this.getValue();
if (value.length > 0) {
value += ',';
}
value += customValue;
const obj = {};
obj[property] = value;
obj.assigned = null;
return this.options.app.state.updateFilter(obj);
},

sortValues(values) {
return sortBy(values, v => v.val === '' ? -999999 : -v.count);
},

serializeData() {
return {
...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 60
server/sonar-web/src/main/js/apps/issues/facets/author-facet.js View File

@@ -1,60 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import CustomValuesFacet from './custom-values-facet';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export default CustomValuesFacet.extend({
getUrl() {
return window.baseUrl + '/api/issues/authors';
},

prepareSearch() {
return this.$('.js-custom-value').select2({
placeholder: translate('search_verb'),
minimumInputLength: 2,
allowClear: false,
formatNoMatches() {
return translate('select2.noMatches');
},
formatSearching() {
return translate('select2.searching');
},
formatInputTooShort() {
return translateWithParameters('select2.tooShort', 2);
},
width: '100%',
ajax: {
quietMillis: 300,
url: this.getUrl(),
data(term) {
return { q: term, ps: 25 };
},
results(data) {
return {
more: false,
results: data.authors.map(author => {
return { id: author, text: author };
})
};
}
}
});
}
});

+ 0
- 41
server/sonar-web/src/main/js/apps/issues/facets/base-facet.js View File

@@ -1,41 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import BaseFacet from '../../../components/navigator/facets/base-facet';
import Template from '../templates/facets/issues-base-facet.hbs';

export default BaseFacet.extend({
template: Template,

onRender() {
BaseFacet.prototype.onRender.apply(this, arguments);
return this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
},

onDestroy() {
return this.$('[data-toggle="tooltip"]').tooltip('destroy');
},

serializeData() {
return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
state: this.options.app.state.toJSON()
};
}
});

+ 0
- 32
server/sonar-web/src/main/js/apps/issues/facets/context-facet.js View File

@@ -1,32 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-context-facet.hbs';

export default BaseFacet.extend({
template: Template,

serializeData() {
return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
state: this.options.app.state.toJSON()
};
}
});

+ 0
- 176
server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js View File

@@ -1,176 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import moment from 'moment';
import { times } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-creation-date-facet.hbs';
import '../../../components/widgets/barchart';
import { formatMeasure } from '../../../helpers/measures';

export default BaseFacet.extend({
template: Template,

events() {
return {
...BaseFacet.prototype.events.apply(this, arguments),
'change input': 'applyFacet',
'click .js-select-period-start': 'selectPeriodStart',
'click .js-select-period-end': 'selectPeriodEnd',
'click .sonar-d3 rect': 'selectBar',
'click .js-all': 'onAllClick',
'click .js-last-week': 'onLastWeekClick',
'click .js-last-month': 'onLastMonthClick',
'click .js-last-year': 'onLastYearClick',
'click .js-leak': 'onLeakClick'
};
},

onRender() {
const that = this;
this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled'));
this.$('input').datepicker({
dateFormat: 'yy-mm-dd',
changeMonth: true,
changeYear: true
});
const props = ['createdAfter', 'createdBefore', 'createdAt'];
const query = this.options.app.state.get('query');
props.forEach(prop => {
const value = query[prop];
if (value != null) {
that.$(`input[name=${prop}]`).val(value);
}
});
let values = this.model.getValues();
if (!(Array.isArray(values) && values.length > 0)) {
let date = moment();
values = [];
times(10, () => {
values.push({ count: 0, val: date.toDate().toString() });
date = date.subtract(1, 'days');
});
values.reverse();
}
values = values.map(v => {
const format = that.options.app.state.getFacetMode() === 'count'
? 'SHORT_INT'
: 'SHORT_WORK_DUR';
const text = formatMeasure(v.count, format);
return { ...v, text };
});
return this.$('.js-barchart').barchart(values);
},

selectPeriodStart() {
return this.$('.js-period-start').datepicker('show');
},

selectPeriodEnd() {
return this.$('.js-period-end').datepicker('show');
},

applyFacet() {
const obj = { createdAt: null, createdInLast: null };
this.$('input').each(function() {
const property = $(this).prop('name');
const value = $(this).val();
obj[property] = value;
});
return this.options.app.state.updateFilter(obj);
},

disable() {
return this.options.app.state.updateFilter({
createdAfter: null,
createdBefore: null,
createdAt: null,
sinceLeakPeriod: null,
createdInLast: null
});
},

selectBar(e) {
const periodStart = $(e.currentTarget).data('period-start');
const periodEnd = $(e.currentTarget).data('period-end');
return this.options.app.state.updateFilter({
createdAfter: periodStart,
createdBefore: periodEnd,
createdAt: null,
sinceLeakPeriod: null,
createdInLast: null
});
},

selectPeriod(period) {
return this.options.app.state.updateFilter({
createdAfter: null,
createdBefore: null,
createdAt: null,
sinceLeakPeriod: null,
createdInLast: period
});
},

onAllClick(e) {
e.preventDefault();
return this.disable();
},

onLastWeekClick(e) {
e.preventDefault();
return this.selectPeriod('1w');
},

onLastMonthClick(e) {
e.preventDefault();
return this.selectPeriod('1m');
},

onLastYearClick(e) {
e.preventDefault();
return this.selectPeriod('1y');
},

onLeakClick(e) {
e.preventDefault();
this.options.app.state.updateFilter({
createdAfter: null,
createdBefore: null,
createdAt: null,
createdInLast: null,
sinceLeakPeriod: 'true'
});
},

serializeData() {
const hasLeak = this.options.app.state.get('contextComponentQualifier') === 'TRK';

return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
hasLeak,
periodStart: this.options.app.state.get('query').createdAfter,
periodEnd: this.options.app.state.get('query').createdBefore,
createdAt: this.options.app.state.get('query').createdAt,
sinceLeakPeriod: this.options.app.state.get('query').sinceLeakPeriod,
createdInLast: this.options.app.state.get('query').createdInLast
};
}
});

+ 0
- 85
server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js View File

@@ -1,85 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-custom-values-facet.hbs';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export default BaseFacet.extend({
template: Template,

events() {
return {
...BaseFacet.prototype.events.apply(this, arguments),
'change .js-custom-value': 'addCustomValue'
};
},

getUrl() {},

onRender() {
BaseFacet.prototype.onRender.apply(this, arguments);
return this.prepareSearch();
},

prepareSearch() {
return this.$('.js-custom-value').select2({
placeholder: translate('search_verb'),
minimumInputLength: 2,
allowClear: false,
formatNoMatches() {
return translate('select2.noMatches');
},
formatSearching() {
return translate('select2.searching');
},
formatInputTooShort() {
return translateWithParameters('select2.tooShort', 2);
},
width: '100%',
ajax: this.prepareAjaxSearch()
});
},

prepareAjaxSearch() {
return {
quietMillis: 300,
url: this.getUrl(),
data(term, page) {
return { s: term, p: page };
},
results(data) {
return { more: data.more, results: data.results };
}
};
},

addCustomValue() {
const property = this.model.get('property');
const customValue = this.$('.js-custom-value').select2('val');
let value = this.getValue();
if (value.length > 0) {
value += ',';
}
value += customValue;
const obj = {};
obj[property] = value;
return this.options.app.state.updateFilter(obj);
}
});

+ 0
- 61
server/sonar-web/src/main/js/apps/issues/facets/file-facet.js View File

@@ -1,61 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-file-facet.hbs';

export default BaseFacet.extend({
template: Template,

onRender() {
BaseFacet.prototype.onRender.apply(this, arguments);
const widths = this.$('.facet-stat')
.map(function() {
return $(this).outerWidth();
})
.get();
const maxValueWidth = Math.max(...widths);
return this.$('.facet-name').css('padding-right', maxValueWidth);
},

getValuesWithLabels() {
const values = this.model.getValues();
const source = this.options.app.facets.components;
values.forEach(v => {
const key = v.val;
let label = null;
if (key) {
const item = source.find(file => file.uuid === key);
if (item != null) {
label = item.longName;
}
}
v.label = label;
});
return values;
},

serializeData() {
return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 40
server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js View File

@@ -1,40 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-issue-key-facet.hbs';

export default BaseFacet.extend({
template: Template,

onRender() {
return this.$el.toggleClass('hidden', !this.options.app.state.get('query').issues);
},

disable() {
return this.options.app.state.updateFilter({ issues: null });
},

serializeData() {
return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
issues: this.options.app.state.get('query').issues
};
}
});

+ 0
- 84
server/sonar-web/src/main/js/apps/issues/facets/language-facet.js View File

@@ -1,84 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import CustomValuesFacet from './custom-values-facet';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export default CustomValuesFacet.extend({
getUrl() {
return window.baseUrl + '/api/languages/list';
},

prepareSearch() {
return this.$('.js-custom-value').select2({
placeholder: translate('search_verb'),
minimumInputLength: 2,
allowClear: false,
formatNoMatches() {
return translate('select2.noMatches');
},
formatSearching() {
return translate('select2.searching');
},
formatInputTooShort() {
return translateWithParameters('select2.tooShort', 2);
},
width: '100%',
ajax: {
quietMillis: 300,
url: this.getUrl(),
data(term) {
return { q: term, ps: 0 };
},
results(data) {
return {
more: false,
results: data.languages.map(lang => {
return { id: lang.key, text: lang.name };
})
};
}
}
});
},

getValuesWithLabels() {
const values = this.model.getValues();
const source = this.options.app.facets.languages;
values.forEach(v => {
const key = v.val;
let label = null;
if (key) {
const item = source.find(lang => lang.key === key);
if (item != null) {
label = item.name;
}
}
v.label = label;
});
return values;
},

serializeData() {
return {
...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 46
server/sonar-web/src/main/js/apps/issues/facets/module-facet.js View File

@@ -1,46 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import BaseFacet from './base-facet';

export default BaseFacet.extend({
getValuesWithLabels() {
const values = this.model.getValues();
const components = this.options.app.facets.components;
values.forEach(v => {
const uuid = v.val;
let label = uuid;
if (uuid) {
const component = components.find(c => c.uuid === uuid);
if (component != null) {
label = component.longName;
}
}
v.label = label;
});
return values;
},

serializeData() {
return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 112
server/sonar-web/src/main/js/apps/issues/facets/project-facet.js View File

@@ -1,112 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import CustomValuesFacet from './custom-values-facet';
import Template from '../templates/facets/issues-projects-facet.hbs';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { areThereCustomOrganizations, getOrganization } from '../../../store/organizations/utils';

export default CustomValuesFacet.extend({
template: Template,

getUrl() {
return window.baseUrl + '/api/components/search';
},

prepareSearchForViews() {
const contextId = this.options.app.state.get('contextComponentUuid');
return {
url: window.baseUrl + '/api/components/tree',
data(term, page) {
return { q: term, p: page, qualifiers: 'TRK', baseComponentId: contextId };
}
};
},

prepareAjaxSearch() {
const options = {
quietMillis: 300,
url: this.getUrl(),
data(term, page) {
return { q: term, p: page, qualifiers: 'TRK' };
},
results: r => ({
more: r.paging.total > r.paging.pageIndex * r.paging.pageSize,
results: r.components.map(component => ({
id: component.id,
text: component.name
}))
})
};
const contextQualifier = this.options.app.state.get('contextComponentQualifier');
if (contextQualifier === 'VW' || contextQualifier === 'SVW') {
Object.assign(options, this.prepareSearchForViews());
}
return options;
},

prepareSearch() {
return this.$('.js-custom-value').select2({
placeholder: translate('search_verb'),
minimumInputLength: 3,
allowClear: false,
formatNoMatches() {
return translate('select2.noMatches');
},
formatSearching() {
return translate('select2.searching');
},
formatInputTooShort() {
return translateWithParameters('select2.tooShort', 3);
},
width: '100%',
ajax: this.prepareAjaxSearch()
});
},

getValuesWithLabels() {
const values = this.model.getValues();
const projects = this.options.app.facets.components;
const displayOrganizations = areThereCustomOrganizations();
values.forEach(v => {
const uuid = v.val;
let label = '';
let organization = null;
if (uuid) {
const project = projects.find(p => p.uuid === uuid);
if (project != null) {
label = project.longName;
organization = displayOrganizations && project.organization
? getOrganization(project.organization)
: null;
}
}
v.label = label;
v.organization = organization;
});
return values;
},

serializeData() {
return {
...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 61
server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js View File

@@ -1,61 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import CustomValuesFacet from './custom-values-facet';

export default CustomValuesFacet.extend({
getUrl() {
return window.baseUrl + '/api/users/search';
},

prepareAjaxSearch() {
return {
quietMillis: 300,
url: this.getUrl(),
data(term, page) {
return { q: term, p: page };
},
results: window.usersToSelect2
};
},

getValuesWithLabels() {
const values = this.model.getValues();
const source = this.options.app.facets.users;
values.forEach(v => {
const key = v.val;
let label = null;
if (key) {
const item = source.find(user => user.login === key);
if (item != null) {
label = item.name;
}
}
v.label = label;
});
return values;
},

serializeData() {
return {
...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 65
server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js View File

@@ -1,65 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import { sortBy } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-resolution-facet.hbs';

export default BaseFacet.extend({
template: Template,

onRender() {
BaseFacet.prototype.onRender.apply(this, arguments);
const value = this.options.app.state.get('query').resolved;
if (value != null && (!value || value === 'false')) {
this.$('.js-facet').filter('[data-unresolved]').addClass('active');
}
},

toggleFacet(e) {
const unresolved = $(e.currentTarget).is('[data-unresolved]');
$(e.currentTarget).toggleClass('active');
if (unresolved) {
const checked = $(e.currentTarget).is('.active');
const value = checked ? 'false' : null;
return this.options.app.state.updateFilter({
resolved: value,
resolutions: null
});
} else {
return this.options.app.state.updateFilter({
resolved: null,
resolutions: this.getValue()
});
}
},

disable() {
return this.options.app.state.updateFilter({
resolved: null,
resolutions: null
});
},

sortValues(values) {
const order = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
return sortBy(values, v => order.indexOf(v.val));
}
});

+ 0
- 95
server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js View File

@@ -1,95 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import CustomValuesFacet from './custom-values-facet';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export default CustomValuesFacet.extend({
prepareSearch() {
let url = window.baseUrl + '/api/rules/search?f=name,langName';
const languages = this.options.app.state.get('query').languages;
if (languages != null) {
url += '&languages=' + languages;
}
return this.$('.js-custom-value').select2({
placeholder: translate('search_verb'),
minimumInputLength: 2,
allowClear: false,
formatNoMatches() {
return translate('select2.noMatches');
},
formatSearching() {
return translate('select2.searching');
},
formatInputTooShort() {
return translateWithParameters('select2.tooShort', 2);
},
width: '100%',
ajax: {
url,
quietMillis: 300,
data(term, page) {
return { q: term, p: page };
},
results(data) {
const results = data.rules.map(rule => {
const lang = rule.langName || translate('manual');
return {
id: rule.key,
text: '(' + lang + ') ' + rule.name
};
});
return {
more: data.p * data.ps < data.total,
results
};
}
}
});
},

getValuesWithLabels() {
const values = this.model.getValues();
const rules = this.options.app.facets.rules;
values.forEach(v => {
const key = v.val;
let label = '';
let extra = '';
if (key) {
const rule = rules.find(r => r.key === key);
if (rule != null) {
label = rule.name;
}
if (rule != null) {
extra = rule.langName;
}
}
v.label = label;
v.extra = extra;
});
return values;
},

serializeData() {
return {
...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 31
server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js View File

@@ -1,31 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { sortBy } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-severity-facet.hbs';

export default BaseFacet.extend({
template: Template,

sortValues(values) {
const order = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
return sortBy(values, v => order.indexOf(v.val));
}
});

+ 0
- 31
server/sonar-web/src/main/js/apps/issues/facets/status-facet.js View File

@@ -1,31 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { sortBy } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-status-facet.hbs';

export default BaseFacet.extend({
template: Template,

sortValues(values) {
const order = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
return sortBy(values, v => order.indexOf(v.val));
}
});

+ 0
- 72
server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js View File

@@ -1,72 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import CustomValuesFacet from './custom-values-facet';
import { translate } from '../../../helpers/l10n';

export default CustomValuesFacet.extend({
prepareSearch() {
let url = window.baseUrl + '/api/issues/tags?ps=10';
const tags = this.options.app.state.get('query').tags;
if (tags != null) {
url += '&tags=' + tags;
}
return this.$('.js-custom-value').select2({
placeholder: translate('search_verb'),
minimumInputLength: 0,
allowClear: false,
formatNoMatches() {
return translate('select2.noMatches');
},
formatSearching() {
return translate('select2.searching');
},
width: '100%',
ajax: {
url,
quietMillis: 300,
data(term) {
return { q: term, ps: 10 };
},
results(data) {
const results = data.tags.map(tag => {
return { id: tag, text: tag };
});
return { more: false, results };
}
}
});
},

getValuesWithLabels() {
const values = this.model.getValues();
values.forEach(v => {
v.label = v.val;
v.extra = '';
});
return values;
},

serializeData() {
return {
...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValuesWithLabels())
};
}
});

+ 0
- 31
server/sonar-web/src/main/js/apps/issues/facets/type-facet.js View File

@@ -1,31 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { sortBy } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/issues-type-facet.hbs';

export default BaseFacet.extend({
template: Template,

sortValues(values) {
const order = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
return sortBy(values, v => order.indexOf(v.val));
}
});

+ 0
- 87
server/sonar-web/src/main/js/apps/issues/init.js View File

@@ -1,87 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
import State from './models/state';
import Layout from './layout';
import Issues from './models/issues';
import Facets from '../../components/navigator/models/facets';
import Controller from './controller';
import Router from './router';
import WorkspaceListView from './workspace-list-view';
import WorkspaceHeaderView from './workspace-header-view';
import FacetsView from './facets-view';
import HeaderView from './HeaderView';

const App = new Marionette.Application();
const init = function({ el, user }) {
this.state = new State({ user, canBulkChange: user.isLoggedIn });
this.list = new Issues();
this.facets = new Facets();

this.layout = new Layout({ app: this, el });
this.layout.render();
$('#footer').addClass('search-navigator-footer');

this.controller = new Controller({ app: this });

this.issuesView = new WorkspaceListView({
app: this,
collection: this.list
});
this.layout.workspaceListRegion.show(this.issuesView);
this.issuesView.bindScrollEvents();

this.workspaceHeaderView = new WorkspaceHeaderView({
app: this,
collection: this.list
});
this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);

this.facetsView = new FacetsView({
app: this,
collection: this.facets
});
this.layout.facetsRegion.show(this.facetsView);

this.headerView = new HeaderView({
app: this
});
this.layout.filtersRegion.show(this.headerView);

key.setScope('list');
App.router = new Router({ app: App });
Backbone.history.start();
};

App.on('start', el => {
init.call(App, el);
});

export default function(el, user) {
App.start({ el, user });

return () => {
Backbone.history.stop();
App.layout.destroy();
$('#footer').removeClass('search-navigator-footer');
};
}

+ 0
- 40
server/sonar-web/src/main/js/apps/issues/issue-filter-view.js View File

@@ -1,40 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import ActionOptionsView from '../../components/common/action-options-view';
import Template from './templates/issues-issue-filter-form.hbs';

export default ActionOptionsView.extend({
template: Template,

selectOption(e) {
const property = $(e.currentTarget).data('property');
const value = $(e.currentTarget).data('value');
this.trigger('select', property, value);
ActionOptionsView.prototype.selectOption.apply(this, arguments);
},

serializeData() {
return {
...ActionOptionsView.prototype.serializeData.apply(this, arguments),
s: this.model.get('severity')
};
}
});

+ 0
- 62
server/sonar-web/src/main/js/apps/issues/layout.js View File

@@ -1,62 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
import Marionette from 'backbone.marionette';
import Template from './templates/issues-layout.hbs';
import './styles.css';

export default Marionette.LayoutView.extend({
template: Template,

regions: {
filtersRegion: '.issues-header',
facetsRegion: '.search-navigator-facets',
workspaceHeaderRegion: '.search-navigator-workspace-header',
workspaceListRegion: '.search-navigator-workspace-list',
workspaceComponentViewerRegion: '.issues-workspace-component-viewer'
},

onRender() {
this.$('.search-navigator').addClass('sticky');
const top = this.$('.search-navigator').offset().top;
this.$('.search-navigator-workspace-header').css({ top });
this.$('.search-navigator-side').css({ top }).isolatedScroll();
},

showSpinner(region) {
return this[region].show(
new Marionette.ItemView({
template: () => '<i class="spinner"></i>'
})
);
},

showComponentViewer() {
this.scroll = $(window).scrollTop();
this.$('.issues').addClass('issues-extended-view');
},

hideComponentViewer() {
this.$('.issues').removeClass('issues-extended-view');
if (this.scroll != null) {
$(window).scrollTop(this.scroll);
}
}
});

+ 0
- 30
server/sonar-web/src/main/js/apps/issues/models/issue.js View File

@@ -1,30 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import Issue from '../../../components/issue/models/issue';

export default Issue.extend({
reset(attrs, options) {
const keepFields = ['index', 'selected', 'comments'];
keepFields.forEach(field => {
attrs[field] = this.get(field);
});
return Issue.prototype.reset.call(this, attrs, options);
}
});

+ 0
- 108
server/sonar-web/src/main/js/apps/issues/models/issues.js View File

@@ -1,108 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import Backbone from 'backbone';
import Issue from './issue';

export default Backbone.Collection.extend({
model: Issue,

url() {
return window.baseUrl + '/api/issues/search';
},

_injectRelational(issue, source, baseField, lookupField) {
const baseValue = issue[baseField];
if (baseValue != null && Array.isArray(source) && source.length > 0) {
const lookupValue = source.find(candidate => candidate[lookupField] === baseValue);
if (lookupValue != null) {
Object.keys(lookupValue).forEach(key => {
const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1);
issue[newKey] = lookupValue[key];
});
}
}
return issue;
},

_injectCommentsRelational(issue, users) {
if (issue.comments) {
const that = this;
const newComments = issue.comments.map(comment => {
let newComment = { ...comment, author: comment.login };
delete newComment.login;
newComment = that._injectRelational(newComment, users, 'author', 'login');
return newComment;
});
issue = { ...issue, comments: newComments };
}
return issue;
},

_prepareClosed(issue) {
if (issue.status === 'CLOSED') {
issue.flows = [];
delete issue.textRange;
}
return issue;
},

ensureTextRange(issue) {
if (issue.line && !issue.textRange) {
// FIXME 999999
issue.textRange = {
startLine: issue.line,
endLine: issue.line,
startOffset: 0,
endOffset: 999999
};
}
return issue;
},

parseIssues(r, startIndex = 0) {
const that = this;
return r.issues.map((issue, index) => {
Object.assign(issue, { index: startIndex + index });
issue = that._injectRelational(issue, r.components, 'component', 'key');
issue = that._injectRelational(issue, r.components, 'project', 'key');
issue = that._injectRelational(issue, r.components, 'subProject', 'key');
issue = that._injectRelational(issue, r.rules, 'rule', 'key');
issue = that._injectRelational(issue, r.users, 'assignee', 'login');
issue = that._injectCommentsRelational(issue, r.users);
issue = that._prepareClosed(issue);
issue = that.ensureTextRange(issue);
return issue;
});
},

setIndex() {
return this.forEach((issue, index) => issue.set({ index }));
},

selectByKeys(keys) {
const that = this;
keys.forEach(key => {
const issue = that.get(key);
if (issue) {
issue.set({ selected: true });
}
});
}
});

+ 0
- 81
server/sonar-web/src/main/js/apps/issues/models/state.js View File

@@ -1,81 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import State from '../../../components/navigator/models/state';

export default State.extend({
defaults: {
page: 1,
maxResultsReached: false,
query: {},
facets: ['facetMode', 'types', 'resolutions'],
isContext: false,
allFacets: [
'facetMode',
'issues',
'types',
'resolutions',
'severities',
'statuses',
'createdAt',
'rules',
'tags',
'projectUuids',
'moduleUuids',
'directories',
'fileUuids',
'assignees',
'authors',
'languages'
],
facetsFromServer: [
'types',
'severities',
'statuses',
'resolutions',
'projectUuids',
'directories',
'rules',
'moduleUuids',
'tags',
'assignees',
'authors',
'fileUuids',
'languages',
'createdAt'
],
transform: {
resolved: 'resolutions',
assigned: 'assignees',
createdBefore: 'createdAt',
createdAfter: 'createdAt',
sinceLeakPeriod: 'createdAt',
createdInLast: 'createdAt'
}
},

getFacetMode() {
const query = this.get('query');
return query.facetMode || 'count';
},

toJSON() {
return { facetMode: this.getFacetMode(), ...this.attributes };
}
});

+ 55
- 0
server/sonar-web/src/main/js/apps/issues/redirects.js View File

@@ -0,0 +1,55 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import { parseQuery, areMyIssuesSelected, serializeQuery } from './utils';
import type { RawQuery } from './utils';

const parseHash = (hash: string): RawQuery => {
const query: RawQuery = {};
const parts = hash.split('|');
parts.forEach(part => {
const tokens = part.split('=');
if (tokens.length === 2) {
const property = decodeURIComponent(tokens[0]);
const value = decodeURIComponent(tokens[1]);
if (property === 'assigned_to_me' && value === 'true') {
query.myIssues = 'true';
} else {
query[property] = value;
}
}
});
return query;
};

export const onEnter = (state: Object, replace: Function) => {
const { hash } = window.location;
if (hash.length > 1) {
const query = parseHash(hash.substr(1));
const normalizedQuery = {
...serializeQuery(parseQuery(query)),
myIssues: areMyIssuesSelected(query) ? 'true' : undefined
};
replace({
pathname: state.location.pathname,
query: normalizedQuery
});
}
};

+ 0
- 35
server/sonar-web/src/main/js/apps/issues/router.js View File

@@ -1,35 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import Router from '../../components/navigator/router';

export default Router.extend({
routes: {
'': 'home',
':query': 'index'
},

home() {
return this.navigate('resolved=false', { trigger: true, replace: true });
},

index(query) {
this.options.app.state.setQuery(this.options.app.controller.parseQuery(query));
}
});

+ 8
- 5
server/sonar-web/src/main/js/apps/issues/routes.js View File

@@ -17,13 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { onEnter } from './redirects';

const routes = [
{
indexRoute: {
getComponent(_, callback) {
require.ensure([], require =>
callback(null, require('./components/IssuesAppContainer').default));
}
getIndexRoute(_, callback) {
require.ensure([], require =>
callback(null, {
component: require('./components/AppContainer').default,
onEnter
}));
}
}
];

+ 168
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js View File

@@ -0,0 +1,168 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { sortBy, uniq, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import FacetFooter from './components/FacetFooter';
import { searchAssignees } from '../utils';
import type { ReferencedUser, Component } from '../utils';
import Avatar from '../../../components/ui/Avatar';
import { translate } from '../../../helpers/l10n';

type Props = {|
assigned: boolean,
assignees: Array<string>,
component?: Component,
facetMode: string,
onChange: (changes: {}) => void,
onToggle: (property: string) => void,
open: boolean,
stats?: { [string]: number },
referencedUsers: { [string]: ReferencedUser }
|};

export default class AssigneeFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'assignees';

handleItemClick = (itemValue: string) => {
if (itemValue === '') {
// unassigned
this.props.onChange({ assigned: !this.props.assigned, assignees: [] });
} else {
// defined assignee
const { assignees } = this.props;
const newValue = sortBy(
assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue]
);
this.props.onChange({ assigned: true, assignees: newValue });
}
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

handleSearch = (query: string) => searchAssignees(query, this.props.component);

handleSelect = (assignee: string) => {
const { assignees } = this.props;
this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, assignee]) });
};

isAssigneeActive(assignee: string) {
return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee);
}

getAssigneeName(assignee: string): React.Element<*> | string {
if (assignee === '') {
return translate('unassigned');
} else {
const { referencedUsers } = this.props;
if (referencedUsers[assignee]) {
return (
<span>
<Avatar
className="little-spacer-right"
hash={referencedUsers[assignee].avatar}
size={16}
/>
{referencedUsers[assignee].name}
</span>
);
} else {
return assignee;
}
}
}

getStat(assignee: string): ?number {
const { stats } = this.props;
return stats ? stats[assignee] : null;
}

renderOption = (option: { avatar: string, label: string }) => {
return (
<span>
{option.avatar != null &&
<Avatar className="little-spacer-right" hash={option.avatar} size={16} />}
{option.label}
</span>
);
};

render() {
const { stats } = this.props;

if (!stats) {
return null;
}

const assignees = sortBy(
Object.keys(stats),
// put unassigned first
key => key === '' ? 0 : 1,
// the sort by number
key => -stats[key]
);

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={!this.props.assigned || this.props.assignees.length > 0}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

{this.props.open &&
<FacetItemsList>
{assignees.map(assignee => (
<FacetItem
active={this.isAssigneeActive(assignee)}
facetMode={this.props.facetMode}
key={assignee}
name={this.getAssigneeName(assignee)}
onClick={this.handleItemClick}
stat={this.getStat(assignee)}
value={assignee}
/>
))}
</FacetItemsList>}

{this.props.open &&
<FacetFooter
onSearch={this.handleSearch}
onSelect={this.handleSelect}
renderOption={this.renderOption}
/>}
</FacetBox>
);
}
}

+ 99
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js View File

@@ -0,0 +1,99 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { sortBy, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import { translate } from '../../../helpers/l10n';

type Props = {|
facetMode: string,
onChange: (changes: {}) => void,
onToggle: (property: string) => void,
open: boolean,
stats?: { [string]: number },
authors: Array<string>
|};

export default class AuthorFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'authors';

handleItemClick = (itemValue: string) => {
const { authors } = this.props;
const newValue = sortBy(
authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue]
);
this.props.onChange({ [this.property]: newValue });
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

getStat(author: string): ?number {
const { stats } = this.props;
return stats ? stats[author] : null;
}

render() {
const { stats } = this.props;

if (!stats) {
return null;
}

const authors = sortBy(Object.keys(stats), key => -stats[key]);

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.authors.length > 0}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

{this.props.open &&
<FacetItemsList>
{authors.map(author => (
<FacetItem
active={this.props.authors.includes(author)}
facetMode={this.props.facetMode}
key={author}
name={author}
onClick={this.handleItemClick}
stat={this.getStat(author)}
value={author}
/>
))}
</FacetItemsList>}
</FacetBox>
);
}
}

+ 276
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js View File

@@ -0,0 +1,276 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import moment from 'moment';
import { max } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import { BarChart } from '../../../components/charts/bar-chart';
import DateInput from '../../../components/controls/DateInput';
import { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import type { Component } from '../utils';

type Props = {|
component?: Component,
createdAfter: string,
createdAt: string,
createdBefore: string,
createdInLast: string,
facetMode: string,
onChange: (changes: {}) => void,
onToggle: (property: string) => void,
open: boolean,
sinceLeakPeriod: boolean,
stats?: { [string]: number }
|};

const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ';

export default class CreationDateFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'createdAt';

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

resetTo = (changes: {}) => {
this.props.onChange({
createdAfter: undefined,
createdAt: undefined,
createdBefore: undefined,
createdInLast: undefined,
sinceLeakPeriod: undefined,
...changes
});
};

handleBarClick = (
{ createdAfter, createdBefore }: { createdAfter: Object, createdBefore?: Object }
) => {
this.resetTo({
createdAfter: createdAfter.format(DATE_FORMAT),
createdBefore: createdBefore && createdBefore.format(DATE_FORMAT)
});
};

handlePeriodChange = (property: string) =>
(value: string) => {
this.props.onChange({
createdAt: undefined,
createdInLast: undefined,
sinceLeakPeriod: undefined,
[property]: value
});
};

handlePeriodClick = (period?: string) =>
(e: Event & { target: HTMLElement }) => {
e.preventDefault();
e.target.blur;
this.resetTo({ createdInLast: period });
};

handleLeakPeriodClick = () =>
(e: Event & { target: HTMLElement }) => {
e.preventDefault();
e.target.blur;
this.resetTo({ sinceLeakPeriod: true });
};

renderBarChart() {
const { createdBefore, stats } = this.props;

if (!stats) {
return null;
}

const periods = Object.keys(stats);

if (periods.length < 2) {
return null;
}

const data = periods.map((startDate, index) => {
const startMoment = moment(startDate);
const nextStartMoment = index < periods.length - 1
? moment(periods[index + 1])
: createdBefore ? moment(createdBefore) : undefined;
const endMoment = nextStartMoment && nextStartMoment.clone().subtract(1, 'days');

let tooltip = formatMeasure(stats[startDate], 'SHORT_INT') +
'<br>' +
startMoment.format('LL');

if (endMoment) {
const isSameDay = endMoment.diff(startMoment, 'days') <= 1;
if (!isSameDay) {
tooltip += ' – ' + endMoment.format('LL');
}
}

return {
createdAfter: startMoment,
createdBefore: nextStartMoment,
startMoment,
tooltip,
x: index,
y: stats[startDate]
};
});

const barsWidth = Math.floor(240 / data.length);
const width = barsWidth * data.length - 1 + 20;

const maxValue = max(data.map(d => d.y));
const format = this.props.facetMode === 'count' ? 'SHORT_INT' : 'SHORT_WORK_DUR';
const xValues = data.map(d => d.y === maxValue ? formatMeasure(maxValue, format) : '');

return (
<BarChart
barsWidth={barsWidth - 1}
data={data}
height={75}
onBarClick={this.handleBarClick}
padding={[25, 10, 5, 10]}
width={width}
xValues={xValues}
/>
);
}

renderExactDate() {
const m = moment(this.props.createdAt);
return (
<div className="search-navigator-facet-container">
{m.format('LLL')}
<br />
<span className="note">({m.fromNow()})</span>
</div>
);
}

renderPeriodSelectors() {
const { createdAfter, createdBefore } = this.props;

return (
<div className="search-navigator-date-facet-selection">
<DateInput
className="search-navigator-date-facet-selection-dropdown-left"
onChange={this.handlePeriodChange('createdAfter')}
placeholder={translate('from')}
value={createdAfter ? moment(createdAfter).format('YYYY-MM-DD') : undefined}
/>
<DateInput
className="search-navigator-date-facet-selection-dropdown-right"
onChange={this.handlePeriodChange('createdBefore')}
placeholder={translate('to')}
value={createdBefore ? moment(createdBefore).format('YYYY-MM-DD') : undefined}
/>
</div>
);
}

renderPrefefinedPeriods() {
const { component, createdInLast, sinceLeakPeriod } = this.props;
return (
<div className="spacer-top">
<span className="spacer-right">{translate('issues.facet.createdAt.or')}</span>
<a className="spacer-right" href="#" onClick={this.handlePeriodClick()}>
{translate('issues.facet.createdAt.all')}
</a>
{component == null &&
<a
className={classNames('spacer-right', { 'active-link': createdInLast === '1w' })}
href="#"
onClick={this.handlePeriodClick('1w')}>
{translate('issues.facet.createdAt.last_week')}
</a>}
{component == null &&
<a
className={classNames('spacer-right', { 'active-link': createdInLast === '1m' })}
href="#"
onClick={this.handlePeriodClick('1m')}>
{translate('issues.facet.createdAt.last_month')}
</a>}
{component == null &&
<a
className={classNames('spacer-right', { 'active-link': createdInLast === '1y' })}
href="#"
onClick={this.handlePeriodClick('1y')}>
{translate('issues.facet.createdAt.last_year')}
</a>}
{component != null &&
<a
className={classNames('spacer-right', { 'active-link': sinceLeakPeriod })}
href="#"
onClick={this.handleLeakPeriodClick()}>
{translate('issues.leak_period')}
</a>}
</div>
);
}

renderInner() {
const { createdAt } = this.props;
return createdAt
? this.renderExactDate()
: <div>
{this.renderBarChart()}
{this.renderPeriodSelectors()}
{this.renderPrefefinedPeriods()}
</div>;
}

render() {
const hasValue = this.props.createdAfter.length > 0 ||
this.props.createdAt.length > 0 ||
this.props.createdBefore.length > 0 ||
this.props.createdInLast.length > 0 ||
this.props.sinceLeakPeriod;

const { stats } = this.props;

if (!stats) {
return null;
}

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={hasValue}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

{this.props.open && this.renderInner()}
</FacetBox>
);
}
}

+ 120
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js View File

@@ -0,0 +1,120 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { sortBy, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import type { ReferencedComponent } from '../utils';
import QualifierIcon from '../../../components/shared/QualifierIcon';
import { translate } from '../../../helpers/l10n';

type Props = {|
facetMode: string,
onChange: (changes: { [string]: Array<string> }) => void,
onToggle: (property: string) => void,
open: boolean,
stats?: { [string]: number },
referencedComponents: { [string]: ReferencedComponent },
directories: Array<string>
|};

export default class DirectoryFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'directories';

handleItemClick = (itemValue: string) => {
const { directories } = this.props;
const newValue = sortBy(
directories.includes(itemValue)
? without(directories, itemValue)
: [...directories, itemValue]
);
this.props.onChange({ [this.property]: newValue });
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

getStat(directory: string): ?number {
const { stats } = this.props;
return stats ? stats[directory] : null;
}

renderName(directory: string): React.Element<*> | string {
// `referencedComponents` are indexed by uuid
// so we have to browse them all to find a matching one
const { referencedComponents } = this.props;
const uuid = Object.keys(referencedComponents).find(
uuid => referencedComponents[uuid].key === directory
);
const name = uuid ? referencedComponents[uuid].name : directory;
return (
<span>
<QualifierIcon className="little-spacer-right" qualifier="DIR" />
{name}
</span>
);
}

render() {
const { stats } = this.props;

if (!stats) {
return null;
}

const directories = sortBy(Object.keys(stats), key => -stats[key]);

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.directories.length > 0}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

{this.props.open &&
<FacetItemsList>
{directories.map(directory => (
<FacetItem
active={this.props.directories.includes(directory)}
facetMode={this.props.facetMode}
key={directory}
name={this.renderName(directory)}
onClick={this.handleItemClick}
stat={this.getStat(directory)}
value={directory}
/>
))}
</FacetItemsList>}
</FacetBox>
);
}
}

+ 67
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js View File

@@ -0,0 +1,67 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import { translate } from '../../../helpers/l10n';

type Props = {|
facetMode: string,
onChange: (changes: {}) => void
|};

export default class FacetMode extends React.PureComponent {
props: Props;

property = 'facetMode';

handleItemClick = (itemValue: string) => {
this.props.onChange({ [this.property]: itemValue });
};

render() {
const { facetMode } = this.props;
const modes = ['count', 'effort'];

return (
<FacetBox property={this.property}>
<FacetHeader name={translate('issues.facet.mode')} />

<FacetItemsList>
{modes.map(mode => (
<FacetItem
active={facetMode === mode}
facetMode={this.props.facetMode}
halfWidth={true}
key={mode}
name={translate('issues.facet.mode', mode)}
onClick={this.handleItemClick}
stat={null}
value={mode}
/>
))}
</FacetItemsList>
</FacetBox>
);
}
}

+ 116
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js View File

@@ -0,0 +1,116 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { sortBy, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import type { ReferencedComponent } from '../utils';
import QualifierIcon from '../../../components/shared/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';

type Props = {|
facetMode: string,
onChange: (changes: { [string]: Array<string> }) => void,
onToggle: (property: string) => void,
open: boolean,
stats?: { [string]: number },
referencedComponents: { [string]: ReferencedComponent },
files: Array<string>
|};

export default class FileFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'files';

handleItemClick = (itemValue: string) => {
const { files } = this.props;
const newValue = sortBy(
files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue]
);
this.props.onChange({ [this.property]: newValue });
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

getStat(file: string): ?number {
const { stats } = this.props;
return stats ? stats[file] : null;
}

renderName(file: string): React.Element<*> | string {
const { referencedComponents } = this.props;
const name = referencedComponents[file]
? collapsePath(referencedComponents[file].path, 15)
: file;
return (
<span>
<QualifierIcon className="little-spacer-right" qualifier="FIL" />
{name}
</span>
);
}

render() {
const { stats } = this.props;

if (!stats) {
return null;
}

const files = sortBy(Object.keys(stats), key => -stats[key]);

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.files.length > 0}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

{this.props.open &&
<FacetItemsList>
{files.map(file => (
<FacetItem
active={this.props.files.includes(file)}
facetMode={this.props.facetMode}
key={file}
name={this.renderName(file)}
onClick={this.handleItemClick}
stat={this.getStat(file)}
value={file}
/>
))}
</FacetItemsList>}
</FacetBox>
);
}
}

+ 114
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js View File

@@ -0,0 +1,114 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { sortBy, uniq, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import LanguageFacetFooter from './LanguageFacetFooter';
import type { ReferencedLanguage } from '../utils';
import { translate } from '../../../helpers/l10n';

type Props = {|
facetMode: string,
onChange: (changes: { [string]: Array<string> }) => void,
onToggle: (property: string) => void,
open: boolean,
stats?: { [string]: number },
referencedLanguages: { [string]: ReferencedLanguage },
languages: Array<string>
|};

export default class LanguageFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'languages';

handleItemClick = (itemValue: string) => {
const { languages } = this.props;
const newValue = sortBy(
languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue]
);
this.props.onChange({ [this.property]: newValue });
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

getLanguageName(language: string): string {
const { referencedLanguages } = this.props;
return referencedLanguages[language] ? referencedLanguages[language].name : language;
}

getStat(language: string): ?number {
const { stats } = this.props;
return stats ? stats[language] : null;
}

handleSelect = (language: string) => {
const { languages } = this.props;
this.props.onChange({ [this.property]: uniq([...languages, language]) });
};

render() {
const { stats } = this.props;

if (!stats) {
return null;
}

const languages = sortBy(Object.keys(stats), key => -stats[key]);

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.languages.length > 0}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

{this.props.open &&
<FacetItemsList>
{languages.map(language => (
<FacetItem
active={this.props.languages.includes(language)}
facetMode={this.props.facetMode}
key={language}
name={this.getLanguageName(language)}
onClick={this.handleItemClick}
stat={this.getStat(language)}
value={language}
/>
))}
</FacetItemsList>}

{this.props.open && <LanguageFacetFooter onSelect={this.handleSelect} />}
</FacetBox>
);
}
}

+ 68
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js View File

@@ -0,0 +1,68 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import Select from 'react-select';
import { connect } from 'react-redux';
import { translate } from '../../../helpers/l10n';
import { getLanguages } from '../../../store/rootReducer';

type Option = { label: string, value: string };

type Props = {|
languages: Array<{ key: string, name: string }>,
onSelect: (value: string) => void
|};

class LanguageFacetFooter extends React.PureComponent {
props: Props;

handleChange = (option: Option) => {
this.props.onSelect(option.value);
};

render() {
const options = this.props.languages.map(language => ({
label: language.name,
value: language.key
}));

return (
<div className="search-navigator-facet-footer">
<Select
autofocus={true}
className="input-super-large"
clearable={false}
noResultsText={translate('select2.noMatches')}
onChange={this.handleChange}
options={options}
placeholder={translate('search_verb')}
searchable={true}
/>
</div>
);
}
}

const mapStateToProps = state => ({
languages: Object.values(getLanguages(state))
});

export default connect(mapStateToProps)(LanguageFacetFooter);

+ 113
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js View File

@@ -0,0 +1,113 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { sortBy, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import type { ReferencedComponent } from '../utils';
import QualifierIcon from '../../../components/shared/QualifierIcon';
import { translate } from '../../../helpers/l10n';

type Props = {|
facetMode: string,
onChange: (changes: { [string]: Array<string> }) => void,
onToggle: (property: string) => void,
open: boolean,
stats?: { [string]: number },
referencedComponents: { [string]: ReferencedComponent },
modules: Array<string>
|};

export default class ModuleFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'modules';

handleItemClick = (itemValue: string) => {
const { modules } = this.props;
const newValue = sortBy(
modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue]
);
this.props.onChange({ [this.property]: newValue });
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

getStat(module: string): ?number {
const { stats } = this.props;
return stats ? stats[module] : null;
}

renderName(module: string): React.Element<*> | string {
const { referencedComponents } = this.props;
const name = referencedComponents[module] ? referencedComponents[module].name : module;
return (
<span>
<QualifierIcon className="little-spacer-right" qualifier="BRC" />
{name}
</span>
);
}

render() {
const { stats } = this.props;

if (!stats) {
return null;
}

const modules = sortBy(Object.keys(stats), key => -stats[key]);

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.modules.length > 0}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

{this.props.open &&
<FacetItemsList>
{modules.map(module => (
<FacetItem
active={this.props.modules.includes(module)}
facetMode={this.props.facetMode}
key={module}
name={this.renderName(module)}
onClick={this.handleItemClick}
stat={this.getStat(module)}
value={module}
/>
))}
</FacetItemsList>}
</FacetBox>
);
}
}

+ 0
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save