Browse Source

rewrite rules app with react (#2982)

tags/7.5
Stas Vilchik 6 years ago
parent
commit
cebce15815
No account linked to committer's email address
100 changed files with 5693 additions and 3678 deletions
  1. 13
    0
      server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/QProfileTester.java
  2. 0
    1
      server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/QualityProfilePage.java
  3. 231
    3
      server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleDetails.java
  4. 14
    5
      server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java
  5. 86
    13
      server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RulesPage.java
  6. 1
    0
      server/sonar-web/package.json
  7. 18
    3
      server/sonar-web/src/main/js/api/issues.ts
  8. 57
    0
      server/sonar-web/src/main/js/api/quality-profiles.ts
  9. 73
    12
      server/sonar-web/src/main/js/api/rules.ts
  10. 5
    5
      server/sonar-web/src/main/js/app/styles/components/search-navigator.css
  11. 5
    5
      server/sonar-web/src/main/js/app/styles/init/forms.css
  12. 67
    0
      server/sonar-web/src/main/js/app/types.ts
  13. 1
    1
      server/sonar-web/src/main/js/apps/about/components/AboutStandards.js
  14. 0
    132
      server/sonar-web/src/main/js/apps/coding-rules/bulk-change-modal-view.js
  15. 0
    57
      server/sonar-web/src/main/js/apps/coding-rules/bulk-change-popup-view.js
  16. 86
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx
  17. 259
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
  18. 48
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx
  19. 583
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
  20. 79
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx
  21. 154
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
  22. 256
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
  23. 0
    94
      server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js
  24. 99
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx
  25. 83
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.tsx
  26. 415
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
  27. 19
    22
      server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx
  28. 123
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
  29. 146
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
  30. 51
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/InheritanceFacet.tsx
  31. 75
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx
  32. 54
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx
  33. 71
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/PageActions.tsx
  34. 171
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
  35. 60
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx
  36. 76
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx
  37. 242
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
  38. 179
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx
  39. 216
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
  40. 159
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx
  41. 236
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
  42. 55
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx
  43. 293
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx
  44. 90
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
  45. 44
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleInheritanceIcon.tsx
  46. 216
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
  47. 133
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx
  48. 13
    21
      server/sonar-web/src/main/js/apps/coding-rules/components/StatusFacet.tsx
  49. 64
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx
  50. 62
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx
  51. 25
    5
      server/sonar-web/src/main/js/apps/coding-rules/components/TypeFacet.tsx
  52. 0
    69
      server/sonar-web/src/main/js/apps/coding-rules/confirm-dialog.js
  53. 0
    200
      server/sonar-web/src/main/js/apps/coding-rules/controller.js
  54. 0
    57
      server/sonar-web/src/main/js/apps/coding-rules/facets-view.js
  55. 0
    61
      server/sonar-web/src/main/js/apps/coding-rules/facets/active-severity-facet.js
  56. 0
    57
      server/sonar-web/src/main/js/apps/coding-rules/facets/available-since-facet.js
  57. 0
    26
      server/sonar-web/src/main/js/apps/coding-rules/facets/base-facet.js
  58. 0
    41
      server/sonar-web/src/main/js/apps/coding-rules/facets/custom-labels-facet.js
  59. 0
    87
      server/sonar-web/src/main/js/apps/coding-rules/facets/custom-values-facet.js
  60. 0
    87
      server/sonar-web/src/main/js/apps/coding-rules/facets/inheritance-facet.js
  61. 0
    40
      server/sonar-web/src/main/js/apps/coding-rules/facets/key-facet.js
  62. 0
    63
      server/sonar-web/src/main/js/apps/coding-rules/facets/language-facet.js
  63. 0
    132
      server/sonar-web/src/main/js/apps/coding-rules/facets/quality-profile-facet.js
  64. 0
    83
      server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js
  65. 0
    70
      server/sonar-web/src/main/js/apps/coding-rules/facets/repository-facet.js
  66. 0
    32
      server/sonar-web/src/main/js/apps/coding-rules/facets/severity-facet.js
  67. 0
    46
      server/sonar-web/src/main/js/apps/coding-rules/facets/tag-facet.js
  68. 0
    48
      server/sonar-web/src/main/js/apps/coding-rules/facets/template-facet.js
  69. 0
    31
      server/sonar-web/src/main/js/apps/coding-rules/facets/type-facet.js
  70. 0
    129
      server/sonar-web/src/main/js/apps/coding-rules/init.js
  71. 0
    55
      server/sonar-web/src/main/js/apps/coding-rules/layout.js
  72. 0
    49
      server/sonar-web/src/main/js/apps/coding-rules/models/rule.js
  73. 0
    59
      server/sonar-web/src/main/js/apps/coding-rules/models/rules.js
  74. 0
    39
      server/sonar-web/src/main/js/apps/coding-rules/models/state.js
  75. 160
    0
      server/sonar-web/src/main/js/apps/coding-rules/query.ts
  76. 27
    2
      server/sonar-web/src/main/js/apps/coding-rules/routes.ts
  77. 0
    185
      server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
  78. 0
    41
      server/sonar-web/src/main/js/apps/coding-rules/rule-filter-view.js
  79. 0
    224
      server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js
  80. 0
    55
      server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js
  81. 0
    62
      server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js
  82. 0
    37
      server/sonar-web/src/main/js/apps/coding-rules/rule/delete-rule-view.js
  83. 0
    178
      server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js
  84. 0
    107
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-description-view.js
  85. 0
    42
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js
  86. 0
    97
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js
  87. 0
    119
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js
  88. 0
    34
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-parameters-view.js
  89. 0
    180
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profile-view.js
  90. 0
    101
      server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js
  91. 0
    3
      server/sonar-web/src/main/js/apps/coding-rules/styles.css
  92. 0
    41
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-modal.hbs
  93. 0
    41
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-popup.hbs
  94. 0
    20
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-layout.hbs
  95. 0
    14
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-details.hbs
  96. 0
    38
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-filter-form.hbs
  97. 0
    41
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-header.hbs
  98. 0
    71
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list-item.hbs
  99. 0
    5
      server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list.hbs
  100. 0
    0
      server/sonar-web/src/main/js/apps/coding-rules/templates/facets/_coding-rules-facet-header.hbs

+ 13
- 0
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/QProfileTester.java View File

@@ -77,6 +77,19 @@ public class QProfileTester {
return this;
}

public QProfileTester activateRule(QualityProfile profile, String ruleKey, String severity) {
return activateRule(profile.getKey(), ruleKey, severity);
}

public QProfileTester activateRule(String profileKey, String ruleKey, String severity) {
ActivateRuleRequest request = new ActivateRuleRequest()
.setKey(profileKey)
.setRule(ruleKey)
.setSeverity(severity);
service().activateRule(request);
return this;
}

public QProfileTester deactivateRule(QualityProfile profile, String ruleKey) {
service().deactivateRule(new DeactivateRuleRequest().setKey(profile.getKey()).setRule(ruleKey));
return this;

+ 0
- 1
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/QualityProfilePage.java View File

@@ -37,7 +37,6 @@ public class QualityProfilePage {
public RulesPage showMissingSonarWayRules() {
Selenide.$(".quality-profile-rules-sonarway-missing")
.shouldBe(Condition.visible).$("a").click();
Selenide.$(".coding-rules").shouldBe(Condition.visible);
return Selenide.page(RulesPage.class);
}


+ 231
- 3
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleDetails.java View File

@@ -19,15 +19,243 @@
*/
package org.sonarqube.qa.util.pageobjects;

import com.codeborne.selenide.Selenide;
import com.codeborne.selenide.Condition;
import com.codeborne.selenide.SelenideElement;
import java.util.Locale;

import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;

public class RuleDetails {
RuleDetails() {
$(".coding-rule-details").shouldBe(visible);
}

public RuleDetails shouldHaveType(String type) {
$(".coding-rules-detail-property[data-meta=\"type\"]").shouldHave(text(type));
return this;
}

public RuleDetails shouldHaveSeverity(String severity) {
$(".coding-rules-detail-property[data-meta=\"severity\"]").shouldHave(text(severity));
return this;
}

public RuleDetails shouldHaveDescription(String description) {
$(".js-rule-description").shouldHave(text(description));
return this;
}

public RuleDetails shouldBeActivatedOn(String profileKey) {
$("#coding-rules-detail-quality-profiles [data-profile=\"" + profileKey + "\"]").shouldBe(visible);
return this;
}

public RuleDetails shouldNotBeActivatedOn(String profileName) {
$("#coding-rules-detail-quality-profiles").shouldNotHave(text(profileName));
return this;
}

public RuleDetails shouldHaveTotalIssues(int issues) {
$(".js-rule-issues h3").shouldHave(text(String.valueOf(issues)));
return this;
}

public RuleDetails shouldHaveIssuesOnProject(String projectName, int issues) {
$(".coding-rules-most-violated-projects").shouldHave(
Condition.and("", text(projectName), text(String.valueOf(issues))));
return this;
}

public RuleDetails shouldHaveCustomRule(String ruleKey) {
takeCustomRule(ruleKey).shouldBe(visible);
return this;
}

public RuleDetails shouldNotHaveCustomRule(String ruleKey) {
takeCustomRule(ruleKey).shouldNotBe(visible);
return this;
}

public RuleDetails createCustomRule(String ruleName) {
$(".js-create-custom-rule").click();
modal().shouldBe(visible);

$("#coding-rules-custom-rule-creation-name").val(ruleName);
$("#coding-rules-custom-rule-creation-html-description").val("description");
$("#coding-rules-custom-rule-creation-create").click();

modal().shouldNotBe(visible);
return this;
}

public RuleDetails reactivateCustomRule(String ruleName) {
$(".js-create-custom-rule").click();
modal().shouldBe(visible);

$("#coding-rules-custom-rule-creation-name").val(ruleName);
$("#coding-rules-custom-rule-creation-html-description").val("description");
$("#coding-rules-custom-rule-creation-create").click();

modal().find(".alert-warning").shouldBe(visible);
$("#coding-rules-custom-rule-creation-reactivate").click();

modal().shouldNotBe(visible);
return this;
}

public RuleDetails deleteCustomRule(String ruleKey) {
takeCustomRule(ruleKey).$(".js-delete-custom-rule").click();
modal().shouldBe(visible);
modal().find("button").click();
modal().shouldNotBe(visible);
return this;
}

public RuleActivation activate() {
$("#coding-rules-quality-profile-activate").click();
modal().shouldBe(visible);
return new RuleActivation();
}

private static SelenideElement modal() {
return $(".modal");
}

private static SelenideElement takeCustomRule(String ruleKey) {
return $("#coding-rules-detail-custom-rules tr[data-rule=\"" + ruleKey + "\"]");
}

private static SelenideElement getActiveProfileElement(String profileKey) {
return $("#coding-rules-detail-quality-profiles [data-profile=\"" + profileKey + "\"]");
}

public ExtendedDescription extendDescription() {
return new ExtendedDescription().start();
}

public Tags tags() {
return new Tags();
}

public RuleActivation changeActivationOn(String profileKey) {
getActiveProfileElement(profileKey).$(".coding-rules-detail-quality-profile-change").click();
modal().shouldBe(visible);
return new RuleActivation();
}

public RuleDetails activationShouldHaveParameter(String profileKey, String parameter, String value) {
getActiveProfileElement(profileKey).$$(".coding-rules-detail-quality-profile-parameter")
.findBy(Condition.and("", text(parameter), text(value)))
.shouldBe(visible);
return this;
}

public RuleDetails activationShouldHaveSeverity(String profileKey, String severity) {
getActiveProfileElement(profileKey).$(".coding-rules-detail-quality-profile-severity .icon-severity-" + severity.toLowerCase(Locale.ENGLISH)).shouldBe(visible);
return this;
}

public RuleDetails shouldBeActivatedOn(String profileName) {
Selenide.$("#coding-rules-detail-quality-profiles").shouldHave(text(profileName));
public RuleDetails revertActivationToParentDefinition(String profileKey) {
getActiveProfileElement(profileKey).$(".coding-rules-detail-quality-profile-revert").click();
modal().shouldBe(visible);
$(".modal button").click();
modal().shouldNotBe(visible);
return this;
}

public static class ExtendedDescription {
public ExtendedDescription start() {
$("#coding-rules-detail-extend-description").click();
textArea().shouldBe(visible);
return this;
}

public ExtendedDescription cancel() {
$("#coding-rules-detail-extend-description-cancel").click();
textArea().shouldNotBe(visible);
return this;
}

public ExtendedDescription type(String text) {
textArea().val(text);
return this;
}

public ExtendedDescription submit() {
$("#coding-rules-detail-extend-description-submit").click();
textArea().shouldNotBe(visible);
return this;
}

public ExtendedDescription remove() {
$("#coding-rules-detail-extend-description-remove").click();
modal().shouldBe(visible);
$("#coding-rules-detail-extend-description-remove-submit").click();
modal().shouldNotBe(visible);
textArea().shouldNotBe(visible);
return this;
}

private static SelenideElement textArea() {
return $("#coding-rules-detail-extend-description-text");
}
}

public static class Tags {
public Tags shouldHaveNoTags() {
element().shouldHave(text("No tags"));
return this;
}

public Tags shouldHaveTags(String... tags) {
for (String tag : tags) {
element().shouldHave(text(tag));
}
return this;
}

public Tags edit() {
element().$("button").click();
return this;
}

public Tags select(String tag) {
element().$$(".menu a").findBy(text(tag)).click();
return this;
}

public Tags search(String query) {
element().$(".search-box-input").val(query);
return this;
}

public Tags done() {
element().$(".search-box-input").pressEscape();
return this;
}

private static SelenideElement element() {
return $(".coding-rules-detail-property[data-meta=\"tags\"]");
}
}

public static class RuleActivation {
public RuleActivation select(String profileKey) {
$(".modal .js-profile .Select-input input").val(profileKey).pressEnter();
return this;
}

public RuleActivation fill(String parameter, String value) {
$(".modal-field input[name=\"" + parameter + "\"]").val(value);
return this;
}

public RuleActivation save() {
$(".modal button").click();
modal().shouldNotBe(visible);
return this;
}
}
}

+ 14
- 5
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java View File

@@ -21,21 +21,30 @@ package org.sonarqube.qa.util.pageobjects;

import com.codeborne.selenide.SelenideElement;

import static com.codeborne.selenide.Condition.visible;

public class RuleItem {

private final SelenideElement elt;

public RuleItem(SelenideElement elt) {
RuleItem(SelenideElement elt) {
this.elt = elt;
}

public SelenideElement getTitle() {
return elt.$(".coding-rule-title");
public RuleItem filterSimilarRules(String field) {
elt.$(".js-rule-filter").click();
elt.$(".dropdown-menu a[data-field=\"" + field + "\"]").click();
return this;
}

public SelenideElement getMetadata() {
return elt.$(".coding-rule-meta");
public RuleDetails open() {
elt.$(".coding-rule-title a").click();
return new RuleDetails();
}

public RuleItem shouldDisplayDeactivate() {
elt.$(".coding-rules-detail-quality-profile-deactivate").shouldBe(visible);
return this;
}

}

+ 86
- 13
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RulesPage.java View File

@@ -21,46 +21,119 @@ package org.sonarqube.qa.util.pageobjects;

import com.codeborne.selenide.Condition;
import com.codeborne.selenide.ElementsCollection;
import com.codeborne.selenide.Selenide;
import com.codeborne.selenide.SelenideElement;
import org.openqa.selenium.By;

import static com.codeborne.selenide.Condition.exist;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$$;

public class RulesPage extends Navigation {

public RulesPage() {
Selenide.$(By.cssSelector(".coding-rules")).should(Condition.exist);
$("#coding-rules-page").should(exist);
}

public int getTotal() {
// warning - number is localized
return Integer.parseInt(Selenide.$("#coding-rules-total").text());
return Integer.parseInt($("#coding-rules-total").text());
}

public ElementsCollection getSelectedFacetItems(String facetName) {
SelenideElement facet = Selenide.$(".search-navigator-facet-box[data-property='"+ facetName+"']").shouldBe(Condition.visible);
return facet.$$(".js-facet.active");
return getFacetElement(facetName).$$(".facet.active");
}

public RulesPage shouldHaveTotalRules(Integer total) {
Selenide.$("#coding-rules-total").shouldHave(Condition.text(total.toString()));
$(".js-page-counter-total").shouldHave(Condition.text(total.toString()));
return this;
}

public RulesPage shouldDisplayRules(String... ruleKeys) {
for (String key : ruleKeys) {
getRuleElement(key).shouldBe(visible);
}
return this;
}

public RulesPage shouldNotDisplayRules(String... ruleKeys) {
for (String key : ruleKeys) {
getRuleElement(key).shouldNotBe(visible);
}
return this;
}

public RulesPage openFacet(String facet) {
Selenide.$(".search-navigator-facet-box[data-property=\"" + facet + "\"] .js-facet-toggle").click();
getFacetElement(facet).$(".search-navigator-facet-header a").click();
return this;
}

public RulesPage selectFacetItem(String facet, String value) {
getFacetElement(facet).$(".facet[data-facet=\"" + value + "\"]").click();
return this;
}

public RulesPage selectFacetItemByText(String facet, String itemText) {
Selenide.$$(".search-navigator-facet-box[data-property=\"" + facet + "\"] .js-facet")
.findBy(Condition.text(itemText)).click();
public RulesPage selectInactive() {
getFacetElement("profile").$(".active .js-inactive").click();
return this;
}

public RulesPage shouldHaveDisabledFacet(String facet) {
$(".search-navigator-facet-box-forbidden[data-property=\"" + facet + "\"]").shouldBe(visible);
return this;
}

public RulesPage shouldNotHaveDisabledFacet(String facet) {
$(".search-navigator-facet-box-forbidden[data-property=\"" + facet + "\"]").shouldNotBe(visible);
return this;
}

public RuleDetails openFirstRule() {
Selenide.$$(".js-rule").first().click();
Selenide.$(".coding-rules-details").shouldBe(Condition.visible);
$$(".coding-rule-title a").first().click();
return new RuleDetails();
}

public RuleItem takeRule(String ruleKey) {
return new RuleItem(getRuleElement(ruleKey));
}

public RulesPage search(String query) {
$("#coding-rules-search .search-box-input").val(query);
return this;
}

public RulesPage clearAllFilters() {
$("#coding-rules-clear-all-filters").click();
return this;
}

public RulesPage closeDetails() {
$(".js-back").click();
$(".coding-rule-details").shouldNotBe(visible);
return this;
}

public RulesPage activateRule(String ruleKey) {
getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-activate").click();
$(".modal").shouldBe(visible);
$(".modal button").click();
$(".modal").shouldNotBe(visible);
getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-activate").shouldNotBe(visible);
return this;
}

public RulesPage deactivateRule(String ruleKey) {
getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-deactivate").click();
$(".modal button").click();
getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-deactivate").shouldNotBe(visible);
return this;
}

private static SelenideElement getRuleElement(String key) {
return $(".coding-rule[data-rule=\"" + key + "\"]");
}

private static SelenideElement getFacetElement(String facet) {
return $(".search-navigator-facet-box[data-property=\"" + facet + "\"]");
}

}

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

@@ -51,6 +51,7 @@
"@types/escape-html": "0.0.20",
"@types/jest": "22.0.1",
"@types/jquery": "3.2.11",
"@types/keymaster": "1.6.28",
"@types/lodash": "4.14.80",
"@types/prop-types": "15.5.2",
"@types/react": "16.0.29",

+ 18
- 3
server/sonar-web/src/main/js/api/issues.ts View File

@@ -28,7 +28,7 @@ export interface IssueResponse {
}

interface IssuesResponse {
components?: Array<{}>;
components?: { key: string; name: string; uuid: string }[];
debtTotal?: number;
facets: Array<{}>;
issues: RawIssue[];
@@ -38,7 +38,7 @@ interface IssuesResponse {
total: number;
};
rules?: Array<{}>;
users?: Array<{ login: string }>;
users?: { login: string }[];
}

export function searchIssues(query: RequestData): Promise<IssuesResponse> {
@@ -57,7 +57,10 @@ export function getFacets(query: RequestData, facets: string[]): Promise<any> {
});
}

export function getFacet(query: RequestData, facet: string): Promise<any> {
export function getFacet(
query: RequestData,
facet: string
): Promise<{ facet: { count: number; val: string }[]; response: IssuesResponse }> {
return getFacets(query, [facet]).then(r => {
return { facet: r.facets[0].values, response: r.response };
});
@@ -82,6 +85,18 @@ export function getAssignees(query: RequestData): Promise<any> {
return getFacet(query, 'assignees').then(r => extractAssignees(r.facet, r.response));
}

export function extractProjects(facet: { val: string }[], response: IssuesResponse) {
return facet.map(item => {
const project =
response.components && response.components.find(component => component.uuid === item.val);
return { ...item, project };
});
}

export function getProjects(query: RequestData) {
return getFacet(query, 'projectUuids').then(r => extractProjects(r.facet, r.response));
}

export function getIssuesCount(query: RequestData): Promise<any> {
const data = { ...query, ps: 1, facetMode: 'effort' };
return searchIssues(data).then(r => {

+ 57
- 0
server/sonar-web/src/main/js/api/quality-profiles.ts View File

@@ -17,6 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { map } from 'lodash';
import { csvEscape } from '../helpers/csv';
import {
request,
checkStatus,
@@ -219,3 +221,58 @@ export function addGroup(parameters: AddRemoveGroupParameters): Promise<void | R
export function removeGroup(parameters: AddRemoveGroupParameters): Promise<void | Response> {
return post('/api/qualityprofiles/remove_group', parameters).catch(throwGlobalError);
}

export interface BulkActivateParameters {
/* eslint-disable camelcase */
activation?: boolean;
active_severities?: string;
asc?: boolean;
available_since?: string;
compareToProfile?: string;
inheritance?: string;
is_template?: string;
languages?: string;
organization: string | undefined;
q?: string;
qprofile?: string;
repositories?: string;
rule_key?: string;
s?: string;
severities?: string;
statuses?: string;
tags?: string;
targetKey: string;
targetSeverity?: string;
template_key?: string;
types?: string;
/* eslint-enable camelcase */
}

export function bulkActivateRules(data: BulkActivateParameters) {
return postJSON('api/qualityprofiles/activate_rules', data);
}

export function bulkDeactivateRules(data: BulkActivateParameters) {
return postJSON('api/qualityprofiles/deactivate_rules', data);
}

export function activateRule(data: {
key: string;
organization: string | undefined;
params?: { [key: string]: string };
reset?: boolean;
rule: string;
severity?: string;
}) {
const params =
data.params && map(data.params, (value, key) => `${key}=${csvEscape(value)}`).join(';');
return post('/api/qualityprofiles/activate_rule', { ...data, params }).catch(throwGlobalError);
}

export function deactivateRule(data: {
key: string;
organization: string | undefined;
rule: string;
}) {
return post('/api/qualityprofiles/deactivate_rule', data).catch(throwGlobalError);
}

+ 73
- 12
server/sonar-web/src/main/js/api/rules.ts View File

@@ -17,18 +17,34 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { post, getJSON, RequestData } from '../helpers/request';
import { post, getJSON, postJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
import { Rule, RuleDetails, RuleActivation } from '../app/types';

export interface GetRulesAppResponse {
respositories: Array<{ key: string; language: string; name: string }>;
canWrite?: boolean;
repositories: { key: string; language: string; name: string }[];
}

export function getRulesApp(): Promise<GetRulesAppResponse> {
return getJSON('/api/rules/app').catch(throwGlobalError);
export function getRulesApp(data: {
organization: string | undefined;
}): Promise<GetRulesAppResponse> {
return getJSON('/api/rules/app', data).catch(throwGlobalError);
}

export function searchRules(data: RequestData) {
export interface SearchRulesResponse {
actives?: { [rule: string]: RuleActivation[] };
facets?: { property: string; values: { count: number; val: string }[] }[];
p: number;
ps: number;
rules: Rule[];
total: number;
}

export function searchRules(data: {
organization: string | undefined;
[x: string]: any;
}): Promise<SearchRulesResponse> {
return getJSON('/api/rules/search', data).catch(throwGlobalError);
}

@@ -37,20 +53,65 @@ export function takeFacet(response: any, property: string) {
return facet ? facet.values : [];
}

export interface GetRuleDetailsParameters {
export function getRuleDetails(parameters: {
actives?: boolean;
key: string;
organization?: string;
}

export function getRuleDetails(parameters: GetRuleDetailsParameters): Promise<any> {
organization: string | undefined;
}): Promise<{ actives?: RuleActivation[]; rule: RuleDetails }> {
return getJSON('/api/rules/show', parameters).catch(throwGlobalError);
}

export function getRuleTags(parameters: { organization?: string }): Promise<string[]> {
export function getRuleTags(parameters: {
organization: string | undefined;
ps?: number;
q: string;
}): Promise<string[]> {
return getJSON('/api/rules/tags', parameters).then(r => r.tags, throwGlobalError);
}

export function deleteRule(parameters: { key: string }) {
export function createRule(data: {
custom_key: string;
markdown_description: string;
name: string;
organization: string | undefined;
params?: string;
prevent_reactivation?: boolean;
severity?: string;
status?: string;
template_key: string;
type?: string;
}): Promise<RuleDetails> {
return postJSON('/api/rules/create', data).then(
r => r.rule,
error => {
// do not show global error if the status code is 409
// this case should be handled inside a component
if (error && error.response && error.response.status === 409) {
return Promise.reject(error.response);
} else {
return throwGlobalError(error);
}
}
);
}

export function deleteRule(parameters: { key: string; organization: string | undefined }) {
return post('/api/rules/delete', parameters).catch(throwGlobalError);
}

export function updateRule(data: {
key: string;
markdown_description?: string;
markdown_note?: string;
name?: string;
organization: string | undefined;
params?: string;
remediation_fn_base_effort?: string;
remediation_fn_type?: string;
remediation_fy_gap_multiplier?: string;
severity?: string;
status?: string;
tags?: string;
}): Promise<RuleDetails> {
return postJSON('/api/rules/update', data).then(r => r.rule, throwGlobalError);
}

+ 5
- 5
server/sonar-web/src/main/js/app/styles/components/search-navigator.css View File

@@ -99,7 +99,6 @@

.search-navigator-facet-box-forbidden .search-navigator-facet-header {
color: var(--secondFontColor);
font-weight: 400;
}

.search-navigator-facet-box-forbidden .search-navigator-facet-header:hover {
@@ -484,7 +483,7 @@ a.search-navigator-facet:focus .facet-stat {
}

.search-navigator-facet-list {
padding-bottom: 10px;
padding-bottom: var(--gridSize);
font-size: 0;
}

@@ -498,7 +497,7 @@ a.search-navigator-facet:focus .facet-stat {

.search-navigator-facet-footer {
display: block;
padding: 6px 10px;
padding-bottom: var(--gridSize);
border-bottom: none;
}

@@ -689,8 +688,9 @@ a.search-navigator-facet:focus .facet-stat {
}

.search-navigator-filters-header {
float: left;
line-height: 22px;
margin-bottom: 12px;
padding-bottom: 11px;
border-bottom: 1px solid var(--barBorderColor);
}

.search-navigator-filters-name {

+ 5
- 5
server/sonar-web/src/main/js/app/styles/init/forms.css View File

@@ -177,11 +177,11 @@ button:disabled:focus,
.button:disabled:focus,
input[type='submit']:disabled:focus,
input[type='button']:disabled:focus {
color: #bbb;
border-color: #ddd;
background: #ebebeb;
cursor: not-allowed;
box-shadow: none;
color: #bbb !important;
border-color: #ddd !important;
background: #ebebeb !important;
cursor: not-allowed !important;
box-shadow: none !important;
}

.button svg {

+ 67
- 0
server/sonar-web/src/main/js/app/types.ts View File

@@ -17,6 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

// Diff / Omit taken from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766
export type Diff<T extends string, U extends string> = ({ [P in T]: P } &
{ [P in U]: never } & { [x: string]: never })[T];

export type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;

export enum BranchType {
LONG = 'LONG',
SHORT = 'SHORT'
@@ -187,3 +194,63 @@ export interface AppState {
organizationsEnabled?: boolean;
qualifiers: string[];
}

export interface Rule {
isTemplate?: boolean;
key: string;
lang: string;
langName: string;
name: string;
params?: RuleParameter[];
severity: string;
status: string;
sysTags?: string[];
tags?: string[];
type: string;
}

export interface RuleDetails extends Rule {
createdAt: string;
debtOverloaded?: boolean;
debtRemFnCoeff?: string;
debtRemFnOffset?: string;
debtRemFnType?: string;
defaultDebtRemFnOffset?: string;
defaultDebtRemFnType?: string;
defaultRemFnBaseEffort?: string;
defaultRemFnType?: string;
effortToFixDescription?: string;
htmlDesc?: string;
htmlNote?: string;
internalKey?: string;
mdDesc?: string;
mdNote?: string;
remFnBaseEffort?: string;
remFnOverloaded?: boolean;
remFnType?: string;
repo: string;
templateKey?: string;
}

export interface RuleActivation {
createdAt: string;
inherit: RuleInheritance;
params: { key: string; value: string }[];
qProfile: string;
severity: string;
}

export interface RuleParameter {
// TODO is this extra really returned?
extra?: string;
defaultValue?: string;
htmlDesc?: string;
key: string;
type: string;
}

export enum RuleInheritance {
NotInherited = 'NONE',
Inherited = 'INHERITED',
Overridden = 'OVERRIDES'
}

+ 1
- 1
server/sonar-web/src/main/js/apps/about/components/AboutStandards.js View File

@@ -41,7 +41,7 @@ type Props = {
export default function AboutStandards(props /*: Props */) {
const organization = props.appState.organizationsEnabled
? props.appState.defaultOrganization
: null;
: undefined;

return (
<div className="boxed-group">

+ 0
- 132
server/sonar-web/src/main/js/apps/coding-rules/bulk-change-modal-view.js View File

@@ -1,132 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 ModalFormView from '../../components/common/modal-form';
import Template from './templates/coding-rules-bulk-change-modal.hbs';
import { translateWithParameters } from '../../helpers/l10n';
import { postJSON } from '../../helpers/request';

export default ModalFormView.extend({
template: Template,

ui() {
return {
...ModalFormView.prototype.ui.apply(this, arguments),
codingRulesSubmitBulkChange: '#coding-rules-submit-bulk-change'
};
},

showSuccessMessage(profile, succeeded) {
const profileBase = this.options.app.qualityProfiles.find(p => p.key === profile);
const message = translateWithParameters(
'coding_rules.bulk_change.success',
profileBase.name,
profileBase.language,
succeeded
);
this.ui.messagesContainer.append(`<div class="alert alert-success">${message}</div>`);
},

showWarnMessage(profile, succeeded, failed) {
const profileBase = this.options.app.qualityProfiles.find(p => p.key === profile);
const message = translateWithParameters(
'coding_rules.bulk_change.warning',
profileBase.name,
profileBase.language,
succeeded,
failed
);
this.ui.messagesContainer.append(`<div class="alert alert-warning">${message}</div>`);
},

onRender() {
ModalFormView.prototype.onRender.apply(this, arguments);
this.$('#coding-rules-bulk-change-profile').select2({
width: '250px',
minimumResultsForSearch: 1,
openOnEnter: false
});
},

onFormSubmit() {
ModalFormView.prototype.onFormSubmit.apply(this, arguments);
const url = `/api/qualityprofiles/${this.options.action}_rules`;
const options = { ...this.options.app.state.get('query'), wsAction: this.options.action };
const profiles = this.$('#coding-rules-bulk-change-profile').val() || [this.options.param];
this.ui.messagesContainer.empty();
this.sendRequests(url, options, profiles);
},

sendRequests(url, options, profiles) {
const that = this;
let looper = Promise.resolve();
this.disableForm();
profiles.forEach(profile => {
const opts = { ...options, profile_key: profile };
looper = looper.then(() =>
postJSON(url, opts).then(r => {
if (!that.isDestroyed) {
if (r.failed) {
that.showWarnMessage(profile, r.succeeded, r.failed);
} else {
that.showSuccessMessage(profile, r.succeeded);
}
}
})
);
});
looper.then(
() => {
that.options.app.controller.fetchList();
if (!that.isDestroyed) {
that.$(that.ui.codingRulesSubmitBulkChange.selector).hide();
that.enableForm();
that.$('.modal-field').hide();
that.$('.js-modal-close').focus();
}
},
() => {}
);
},

getAvailableQualityProfiles() {
const queryLanguages = this.options.app.state.get('query').languages;
const languages = queryLanguages && queryLanguages.length > 0 ? queryLanguages.split(',') : [];
let profiles = this.options.app.qualityProfiles;
if (languages.length > 0) {
profiles = profiles.filter(profile => languages.indexOf(profile.language) !== -1);
}
return profiles
.filter(profile => profile.actions && profile.actions.edit)
.filter(profile => !profile.isBuiltIn);
},

serializeData() {
const profile = this.options.app.qualityProfiles.find(p => p.key === this.options.param);
return {
...ModalFormView.prototype.serializeData.apply(this, arguments),
action: this.options.action,
state: this.options.app.state.toJSON(),
qualityProfile: this.options.param,
qualityProfileName: profile != null ? profile.name : null,
qualityProfiles: this.options.app.qualityProfiles,
availableQualityProfiles: this.getAvailableQualityProfiles()
};
}
});

+ 0
- 57
server/sonar-web/src/main/js/apps/coding-rules/bulk-change-popup-view.js View File

@@ -1,57 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 PopupView from '../../components/common/popup';
import BulkChangeModalView from './bulk-change-modal-view';
import Template from './templates/coding-rules-bulk-change-popup.hbs';

export default PopupView.extend({
template: Template,

events: {
'click .js-bulk-change': 'doAction'
},

doAction(e) {
const action = $(e.currentTarget).data('action');
const param = $(e.currentTarget).data('param');
new BulkChangeModalView({
app: this.options.app,
action,
param
}).render();
},

serializeData() {
const query = this.options.app.state.get('query');
const profileKey = query.qprofile;
const profile = this.options.app.qualityProfiles.find(p => p.key === profileKey);
const activation = '' + query.activation;
const canChangeProfile =
profile != null && !profile.isBuiltIn && profile.actions && profile.actions.edit;

return {
qualityProfile: profileKey,
qualityProfileName: profile != null ? profile.name : null,
allowActivateOnProfile: canChangeProfile && activation === 'false',
allowDeactivateOnProfile: canChangeProfile && activation === 'true'
};
}
});

+ 86
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx View File

@@ -0,0 +1,86 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import ActivationFormModal from './ActivationFormModal';
import { Profile as BaseProfile } from '../../../api/quality-profiles';
import { Rule, RuleDetails, RuleActivation } from '../../../app/types';

interface Props {
activation?: RuleActivation;
buttonText: string;
className?: string;
modalHeader: string;
onDone: (severity: string) => Promise<void>;
organization: string | undefined;
profiles: BaseProfile[];
rule: Rule | RuleDetails;
updateMode?: boolean;
}

interface State {
modal: boolean;
}

export default class ActivationButton extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { modal: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleButtonClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ modal: true });
};

handleCloseModal = () => this.setState({ modal: false });

render() {
return (
<>
<button
className={this.props.className}
id="coding-rules-quality-profile-activate"
onClick={this.handleButtonClick}>
{this.props.buttonText}
</button>

{this.state.modal && (
<ActivationFormModal
activation={this.props.activation}
modalHeader={this.props.modalHeader}
onClose={this.handleCloseModal}
onDone={this.props.onDone}
organization={this.props.organization}
profiles={this.props.profiles}
rule={this.props.rule}
updateMode={this.props.updateMode}
/>
)}
</>
);
}
}

+ 259
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx View File

@@ -0,0 +1,259 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import Modal from '../../../components/controls/Modal';
import Select from '../../../components/controls/Select';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import Tooltip from '../../../components/controls/Tooltip';
import { activateRule, Profile as BaseProfile } from '../../../api/quality-profiles';
import { Rule, RuleDetails, RuleActivation } from '../../../app/types';
import { SEVERITIES } from '../../../helpers/constants';
import { translate } from '../../../helpers/l10n';
import { sortProfiles } from '../../quality-profiles/utils';

interface Props {
activation?: RuleActivation;
modalHeader: string;
onClose: () => void;
onDone: (severity: string) => Promise<void>;
organization: string | undefined;
profiles: BaseProfile[];
rule: Rule | RuleDetails;
updateMode?: boolean;
}

interface State {
params: { [p: string]: string };
profile: string;
severity: string;
submitting: boolean;
}

export default class ActivationFormModal extends React.PureComponent<Props, State> {
mounted: boolean;

constructor(props: Props) {
super(props);
const profilesWithDepth = this.getQualityProfilesWithDepth(props);
this.state = {
params: this.getParams(props),
profile: profilesWithDepth.length > 0 ? profilesWithDepth[0].key : '',
severity: props.activation ? props.activation.severity : props.rule.severity,
submitting: false
};
}

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

getParams = ({ activation, rule } = this.props) => {
const params: { [p: string]: string } = {};
if (rule && rule.params) {
for (const param of rule.params) {
params[param.key] = param.defaultValue || '';
}
if (activation && activation.params) {
for (const param of activation.params) {
params[param.key] = param.value;
}
}
}
return params;
};

// Choose QP which a user can administrate, which are the same language and which are not built-in
getQualityProfilesWithDepth = ({ profiles } = this.props) =>
sortProfiles(
profiles.filter(
profile =>
!profile.isBuiltIn &&
profile.actions &&
profile.actions.edit &&
profile.language === this.props.rule.lang
)
).map(profile => ({
...profile,
// Decrease depth by 1, so the top level starts at 0
depth: profile.depth - 1
}));

handleCancelClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onClose();
};

handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.setState({ submitting: true });
const data = {
key: this.state.profile,
organization: this.props.organization,
params: this.state.params,
rule: this.props.rule.key,
severity: this.state.severity
};
activateRule(data)
.then(() => this.props.onDone(data.severity))
.then(
() => {
if (this.mounted) {
this.setState({ submitting: false });
this.props.onClose();
}
},
() => {
if (this.mounted) {
this.setState({ submitting: false });
}
}
);
};

handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = event.currentTarget;
this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
};

handleProfileChange = ({ value }: { value: string }) => this.setState({ profile: value });

handleSeverityChange = ({ value }: { value: string }) => this.setState({ severity: value });

renderSeverityOption = ({ value }: { value: string }) => <SeverityHelper severity={value} />;

render() {
const { activation, rule } = this.props;
const { profile, severity, submitting } = this.state;
const { params = [] } = rule;
const profilesWithDepth = this.getQualityProfilesWithDepth();
const isCustomRule = !!(rule as RuleDetails).templateKey;
const activeInAllProfiles = profilesWithDepth.length <= 0;
const isUpdateMode = !!activation;

return (
<Modal contentLabel={this.props.modalHeader} onRequestClose={this.props.onClose}>
<form onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{this.props.modalHeader}</h2>
</div>

<div className="modal-body">
{!isUpdateMode &&
activeInAllProfiles && (
<div className="alert alert-info">
{translate('coding_rules.active_in_all_profiles')}
</div>
)}

<div className="modal-field">
<label>{translate('coding_rules.quality_profile')}</label>
<Select
className="js-profile"
clearable={false}
disabled={submitting || profilesWithDepth.length === 1}
onChange={this.handleProfileChange}
options={profilesWithDepth.map(profile => ({
label: ' '.repeat(profile.depth) + profile.name,
value: profile.key
}))}
value={profile}
/>
</div>
<div className="modal-field">
<label>{translate('severity')}</label>
<Select
className="js-severity"
clearable={false}
disabled={submitting}
onChange={this.handleSeverityChange}
options={SEVERITIES.map(severity => ({
label: translate('severity', severity),
value: severity
}))}
optionRenderer={this.renderSeverityOption}
searchable={false}
value={severity}
valueRenderer={this.renderSeverityOption}
/>
</div>
{isCustomRule ? (
<div className="modal-field">
<p className="note">{translate('coding_rules.custom_rule.activation_notice')}</p>
</div>
) : (
params.map(param => (
<div className="modal-field" key={param.key}>
<Tooltip overlay={param.key} placement="left">
<label>{param.key}</label>
</Tooltip>
{param.type === 'TEXT' ? (
<textarea
className="width100"
disabled={submitting}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
rows={3}
value={this.state.params[param.key] || ''}
/>
) : (
<input
className="input-super-large"
disabled={submitting}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
type="text"
value={this.state.params[param.key] || ''}
/>
)}
<div
className="note"
dangerouslySetInnerHTML={{ __html: param.htmlDesc || '' }}
/>
{param.extra && <div className="note">{param.extra}</div>}
</div>
))
)}
</div>

<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
<button disabled={submitting || activeInAllProfiles} type="submit">
{isUpdateMode ? translate('save') : translate('coding_rules.activate')}
</button>
<button
className="button-link"
disabled={submitting}
onClick={this.handleCancelClick}
type="reset">
{translate('cancel')}
</button>
</footer>
</form>
</Modal>
);
}
}

+ 48
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx View File

@@ -0,0 +1,48 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import Facet, { BasicProps } from './Facet';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { SEVERITIES } from '../../../helpers/constants';
import { translate } from '../../../helpers/l10n';

interface Props extends BasicProps {
disabled: boolean;
}

export default class ActivationSeverityFacet extends React.PureComponent<Props> {
renderName = (severity: string) => <SeverityHelper severity={severity} />;

renderTextName = (severity: string) => translate('severity', severity);

render() {
return (
<Facet
{...this.props}
disabled={this.props.disabled}
disabledHelper={translate('coding_rules.filters.active_severity.inactive')}
options={SEVERITIES}
property="activationSeverities"
renderName={this.renderName}
renderTextName={this.renderTextName}
/>
);
}
}

+ 583
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx View File

@@ -0,0 +1,583 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { Helmet } from 'react-helmet';
import * as PropTypes from 'prop-types';
import { keyBy } from 'lodash';
import * as key from 'keymaster';
import {
Facets,
Query,
parseQuery,
serializeQuery,
areQueriesEqual,
shouldRequestFacet,
FacetKey,
OpenFacets,
getServerFacet,
getAppFacet,
Actives,
Activation,
getOpen
} from '../query';
import { searchRules, getRulesApp } from '../../../api/rules';
import { Paging, Rule, RuleActivation } from '../../../app/types';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../helpers/query';
import ListFooter from '../../../components/controls/ListFooter';
import RuleListItem from './RuleListItem';
import PageActions from './PageActions';
import FiltersHeader from '../../../components/common/FiltersHeader';
import SearchBox from '../../../components/controls/SearchBox';
import FacetsList from './FacetsList';
import { searchQualityProfiles, Profile } from '../../../api/quality-profiles';
import { scrollToElement } from '../../../helpers/scrolling';
import BulkChange from './BulkChange';
import RuleDetails from './RuleDetails';

import '../styles.css';

const PAGE_SIZE = 100;

interface Props {
location: { pathname: string; query: RawQuery };
organization?: { key: string };
}

interface State {
actives?: Actives;
canWrite?: boolean;
facets?: Facets;
loading: boolean;
openFacets: OpenFacets;
openRule?: Rule;
paging?: Paging;
query: Query;
referencedProfiles: { [profile: string]: Profile };
referencedRepositories: { [repository: string]: { key: string; language: string; name: string } };
rules: Rule[];
selected?: string;
}

// TODO redirect to default organization's rules page

export default class App extends React.PureComponent<Props, State> {
mounted: boolean;

static contextTypes = {
organizationsEnabled: PropTypes.bool,
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = {
loading: true,
openFacets: { languages: true, types: true },
query: parseQuery(props.location.query),
referencedProfiles: {},
referencedRepositories: {},
rules: []
};
}

componentDidMount() {
this.mounted = true;
document.body.classList.add('white-page');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.add('page-footer-with-sidebar');
}
this.attachShortcuts();
this.fetchInitialData();
}

componentWillReceiveProps(nextProps: Props) {
const openRule = this.getOpenRule(nextProps, this.state.rules);
if (openRule && openRule.key !== this.state.selected) {
this.setState({ selected: openRule.key });
}
this.setState({ openRule, query: parseQuery(nextProps.location.query) });
}

componentDidUpdate(prevProps: Props, prevState: State) {
if (!areQueriesEqual(prevProps.location.query, this.props.location.query)) {
this.fetchFirstRules();
}
if (
!this.state.openRule &&
(prevState.selected !== this.state.selected || prevState.openRule)
) {
// if user simply selected another issue
// or if he went from the source code back to the list of issues
this.scrollToSelectedRule();
}
}

componentWillUnmount() {
this.mounted = false;
document.body.classList.remove('white-page');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.remove('page-footer-with-sidebar');
}
this.detachShortcuts();
}

attachShortcuts = () => {
key.setScope('coding-rules');
key('up', 'coding-rules', () => {
this.selectPreviousRule();
return false;
});
key('down', 'coding-rules', () => {
this.selectNextRule();
return false;
});
key('right', 'coding-rules', () => {
this.openSelectedRule();
return false;
});
key('left', 'coding-rules', () => {
this.closeRule();
return false;
});
};

detachShortcuts = () => key.deleteScope('coding-rules');

getOpenRule = (props: Props, rules: Rule[]) => {
const open = getOpen(props.location.query);
return open && rules.find(rule => rule.key === open);
};

getFacetsToFetch = () =>
Object.keys(this.state.openFacets)
.filter((facet: FacetKey) => this.state.openFacets[facet])
.filter((facet: FacetKey) => shouldRequestFacet(facet))
.map((facet: FacetKey) => getServerFacet(facet));

getFieldsToFetch = () => {
const fields = [
'isTemplate',
'name',
'lang',
'langName',
'severity',
'status',
'sysTags',
'tags',
'templateKey'
];
if (this.state.query.profile) {
fields.push('actives', 'params');
}
return fields;
};

getSearchParameters = () => ({
f: this.getFieldsToFetch().join(),
facets: this.getFacetsToFetch().join(),
organization: this.props.organization && this.props.organization.key,
ps: PAGE_SIZE,
s: 'name',
...serializeQuery(this.state.query)
});

stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

fetchInitialData = () => {
this.setState({ loading: true });
const organization = this.props.organization && this.props.organization.key;
Promise.all([getRulesApp({ organization }), searchQualityProfiles({ organization })]).then(
([{ canWrite, repositories }, { profiles }]) => {
this.setState({
canWrite,
referencedProfiles: keyBy(profiles, 'key'),
referencedRepositories: keyBy(repositories, 'key')
});
this.fetchFirstRules();
},
this.stopLoading
);
};

makeFetchRequest = (query?: RawQuery) =>
searchRules({ ...this.getSearchParameters(), ...query }).then(
({ actives: rawActives, facets: rawFacets, p, ps, rules, total }) => {
const actives = rawActives && parseActives(rawActives);
const facets = rawFacets && parseFacets(rawFacets);
const paging = { pageIndex: p, pageSize: ps, total };
return { actives, facets, paging, rules };
}
);

fetchFirstRules = (query?: RawQuery) => {
this.setState({ loading: true });
this.makeFetchRequest(query).then(({ actives, facets, paging, rules }) => {
if (this.mounted) {
const openRule = this.getOpenRule(this.props, rules);
const selected = rules.length > 0 ? (openRule && openRule.key) || rules[0].key : undefined;
this.setState({ actives, facets, loading: false, openRule, paging, rules, selected });
}
}, this.stopLoading);
};

fetchMoreRules = () => {
const { paging } = this.state;
if (paging) {
this.setState({ loading: true });
const nextPage = paging.pageIndex + 1;
this.makeFetchRequest({ p: nextPage, facets: undefined }).then(
({ actives, paging, rules }) => {
if (this.mounted) {
this.setState(state => ({
actives: { ...state.actives, actives },
loading: false,
paging,
rules: [...state.rules, ...rules]
}));
}
},
this.stopLoading
);
}
};

fetchFacet = (facet: FacetKey) => {
this.setState({ loading: true });
this.makeFetchRequest({ ps: 1, facets: getServerFacet(facet) }).then(({ facets }) => {
if (this.mounted) {
this.setState(state => ({ facets: { ...state.facets, ...facets }, loading: false }));
}
}, this.stopLoading);
};

getSelectedIndex = ({ selected, rules } = this.state) => {
const index = rules.findIndex(rule => rule.key === selected);
return index !== -1 ? index : undefined;
};

selectNextRule = () => {
const { rules } = this.state;
const selectedIndex = this.getSelectedIndex();
if (rules && selectedIndex !== undefined && selectedIndex < rules.length - 1) {
if (this.state.openRule) {
this.openRule(rules[selectedIndex + 1].key);
} else {
this.setState({ selected: rules[selectedIndex + 1].key });
}
}
};

selectPreviousRule = () => {
const { rules } = this.state;
const selectedIndex = this.getSelectedIndex();
if (rules && selectedIndex !== undefined && selectedIndex > 0) {
if (this.state.openRule) {
this.openRule(rules[selectedIndex - 1].key);
} else {
this.setState({ selected: rules[selectedIndex - 1].key });
}
}
};

getRulePath = (rule: string) => ({
pathname: this.props.location.pathname,
query: { ...serializeQuery(this.state.query), open: rule }
});

openRule = (rule: string) => {
const path = this.getRulePath(rule);
if (this.state.openRule) {
this.context.router.replace(path);
} else {
this.context.router.push(path);
}
};

openSelectedRule = () => {
const { selected } = this.state;
if (selected) {
this.openRule(selected);
}
};

closeRule = () => {
this.context.router.push({
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
open: undefined
}
});
this.scrollToSelectedRule(false);
};

scrollToSelectedRule = (smooth = true) => {
const { selected } = this.state;
if (selected) {
const element = document.querySelector(`[data-rule="${selected}"]`);
if (element) {
scrollToElement(element, { topOffset: 150, bottomOffset: 100, smooth });
}
}
};

getRuleActivation = (rule: string) => {
const { actives, query } = this.state;
if (actives && actives[rule] && query.profile) {
return actives[rule][query.profile];
} else {
return undefined;
}
};

getSelectedProfile = () => {
const { query, referencedProfiles } = this.state;
if (query.profile) {
return referencedProfiles[query.profile];
} else {
return undefined;
}
};

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

handleBack = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeRule();
};

handleFilterChange = (changes: Partial<Query>) =>
this.context.router.push({
pathname: this.props.location.pathname,
query: serializeQuery({ ...this.state.query, ...changes })
});

handleFacetToggle = (facet: keyof Query) => {
this.setState(state => ({
openFacets: { ...state.openFacets, [facet]: !state.openFacets[facet] }
}));
if (shouldRequestFacet(facet) && (!this.state.facets || !this.state.facets[facet])) {
this.fetchFacet(facet);
}
};

handleReload = () => this.fetchFirstRules();

handleReset = () => this.context.router.push({ pathname: this.props.location.pathname });

/** Tries to take rule by index, or takes the last one */
pickRuleAround = (rules: Rule[], selectedIndex: number | undefined) => {
if (selectedIndex === undefined || rules.length === 0) {
return undefined;
}
if (selectedIndex >= 0 && selectedIndex < rules.length) {
return rules[selectedIndex].key;
}
return rules[rules.length - 1].key;
};

handleRuleDelete = (ruleKey: string) => {
if (this.state.query.ruleKey === ruleKey) {
this.handleReset();
} else {
this.setState(state => {
const rules = state.rules.filter(rule => rule.key !== ruleKey);
const selectedIndex = this.getSelectedIndex(state);
const selected = this.pickRuleAround(rules, selectedIndex);
return { rules, selected };
});
this.closeRule();
}
};

handleRuleActivate = (profile: string, rule: string, activation: Activation) =>
this.setState((state: State) => {
const { actives = {} } = state;
if (!actives[rule]) {
return { actives: { ...actives, [rule]: { [profile]: activation } } };
}

return { actives: { ...actives, [rule]: { ...actives[rule], [profile]: activation } } };
});

handleRuleDeactivate = (profile: string, rule: string) =>
this.setState((state: State) => {
const { actives } = state;
if (actives && actives[rule]) {
return { actives: { ...actives, [rule]: { ...actives[rule], [profile]: undefined } } };
}
return {};
});

handleSearch = (searchQuery: string) => this.handleFilterChange({ searchQuery });

isFiltered = () => Object.keys(serializeQuery(this.state.query)).length > 0;

render() {
const { paging, rules } = this.state;
const selectedIndex = this.getSelectedIndex();
const organization = this.props.organization && this.props.organization.key;

return (
<>
<Helmet title={translate('coding_rules.page')} />
<div className="layout-page" id="coding-rules-page">
<ScreenPositionHelper className="layout-page-side-outer">
{({ top }) => (
<div className="layout-page-side" style={{ top }}>
<div className="layout-page-side-inner">
<div className="layout-page-filters">
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
<SearchBox
className="spacer-bottom"
id="coding-rules-search"
onChange={this.handleSearch}
placeholder={translate('search.search_for_rules')}
value={this.state.query.searchQuery || ''}
/>
<FacetsList
facets={this.state.facets}
onFacetToggle={this.handleFacetToggle}
onFilterChange={this.handleFilterChange}
organization={organization}
organizationsEnabled={this.context.organizationsEnabled}
openFacets={this.state.openFacets}
query={this.state.query}
referencedProfiles={this.state.referencedProfiles}
referencedRepositories={this.state.referencedRepositories}
selectedProfile={this.getSelectedProfile()}
/>
</div>
</div>
</div>
)}
</ScreenPositionHelper>

<div className="layout-page-main">
<div className="layout-page-header-panel layout-page-main-header">
<div className="layout-page-header-panel-inner layout-page-main-header-inner">
<div className="layout-page-main-inner">
{this.state.openRule ? (
<a href="#" className="js-back" onClick={this.handleBack}>
{translate('coding_rules.return_to_list')}
</a>
) : (
this.state.paging && (
<BulkChange
organization={organization}
query={this.state.query}
referencedProfiles={this.state.referencedProfiles}
total={this.state.paging.total}
/>
)
)}
<PageActions
loading={this.state.loading}
onReload={this.handleReload}
paging={paging}
selectedIndex={selectedIndex}
/>
</div>
</div>
</div>

<div className="layout-page-main-inner">
{this.state.openRule ? (
<RuleDetails
allowCustomRules={!this.context.organizationsEnabled}
canWrite={this.state.canWrite}
onActivate={this.handleRuleActivate}
onDeactivate={this.handleRuleDeactivate}
onDelete={this.handleRuleDelete}
onFilterChange={this.handleFilterChange}
organization={organization}
referencedProfiles={this.state.referencedProfiles}
referencedRepositories={this.state.referencedRepositories}
ruleKey={this.state.openRule.key}
selectedProfile={this.getSelectedProfile()}
/>
) : (
<>
{rules.map(rule => (
<RuleListItem
activation={this.getRuleActivation(rule.key)}
key={rule.key}
onActivate={this.handleRuleActivate}
onDeactivate={this.handleRuleDeactivate}
onFilterChange={this.handleFilterChange}
organization={organization}
path={this.getRulePath(rule.key)}
rule={rule}
selected={rule.key === this.state.selected}
selectedProfile={this.getSelectedProfile()}
/>
))}
{paging !== undefined && (
<ListFooter
count={rules.length}
loadMore={this.fetchMoreRules}
ready={!this.state.loading}
total={paging.total}
/>
)}
</>
)}
</div>
</div>
</div>
</>
);
}
}

function parseActives(rawActives: { [rule: string]: RuleActivation[] }) {
const actives: Actives = {};
for (const [rule, activations] of Object.entries(rawActives)) {
actives[rule] = {};
for (const { inherit, qProfile, severity } of activations) {
actives[rule][qProfile] = { inherit, severity };
}
}
return actives;
}

function parseFacets(rawFacets: { property: string; values: { count: number; val: string }[] }[]) {
const facets: Facets = {};
for (const rawFacet of rawFacets) {
const values: { [value: string]: number } = {};
for (const rawValue of rawFacet.values) {
values[rawValue.val] = rawValue.count;
}
facets[getAppFacet(rawFacet.property)] = values;
}
return facets;
}

+ 79
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx View File

@@ -0,0 +1,79 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { intlShape } from 'react-intl';
import { Query } from '../query';
import DateInput from '../../../components/controls/DateInput';
import FacetBox from '../../../components/facet/FacetBox';
import FacetHeader from '../../../components/facet/FacetHeader';
import { longFormatterOption } from '../../../components/intl/DateFormatter';
import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
import { serializeDateShort } from '../../../helpers/query';

interface Props {
onChange: (changes: Partial<Query>) => void;
onToggle: (property: keyof Query) => void;
open: boolean;
value?: Date;
}

export default class AvailableSinceFacet extends React.PureComponent<Props> {
static contextTypes = {
intl: intlShape
};

handleHeaderClick = () => this.props.onToggle('availableSince');

handleClear = () => this.props.onChange({ availableSince: undefined });

handlePeriodChange = (value?: string) =>
this.props.onChange({ availableSince: value ? parseDate(value) : undefined });

getValues = () =>
this.props.value
? [this.context.intl.formatDate(this.props.value, longFormatterOption)]
: undefined;

renderDateInput = () => (
<DateInput
name="available-since"
onChange={this.handlePeriodChange}
placeholder={translate('date')}
value={serializeDateShort(this.props.value)}
/>
);

render() {
return (
<FacetBox property="availableSince">
<FacetHeader
name={translate('coding_rules.facet.available_since')}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.getValues()}
/>

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

+ 154
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx View File

@@ -0,0 +1,154 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import * as classNames from 'classnames';
import BulkChangeModal from './BulkChangeModal';
import { Query } from '../query';
import { Profile } from '../../../api/quality-profiles';
import Dropdown from '../../../components/controls/Dropdown';
import { translate } from '../../../helpers/l10n';

interface Props {
organization: string | undefined;
query: Query;
referencedProfiles: { [profile: string]: Profile };
total: number;
}

interface State {
action?: string;
modal: boolean;
profile?: Profile;
}

export default class BulkChange extends React.PureComponent<Props, State> {
closeDropdown: () => void;
state: State = { modal: false };

getSelectedProfile = () => {
const { profile } = this.props.query;
return (profile && this.props.referencedProfiles[profile]) || undefined;
};

closeModal = () => this.setState({ action: undefined, modal: false, profile: undefined });

handleActivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
this.setState({ action: 'activate', modal: true, profile: undefined });
};

handleActivateInProfileClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
this.setState({ action: 'activate', modal: true, profile: this.getSelectedProfile() });
};

handleDeactivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
this.setState({ action: 'deactivate', modal: true, profile: undefined });
};

handleDeactivateInProfileClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
this.setState({ action: 'deactivate', modal: true, profile: this.getSelectedProfile() });
};

render() {
// show "Bulk Change" button only if user has at least one QP which he administrates
const canBulkChange = Object.values(this.props.referencedProfiles).some(profile =>
Boolean(profile.actions && profile.actions.edit)
);
if (!canBulkChange) {
return null;
}

const { activation } = this.props.query;
const profile = this.getSelectedProfile();
const canChangeProfile = Boolean(
profile && !profile.isBuiltIn && profile.actions && profile.actions.edit
);
const allowActivateOnProfile = canChangeProfile && activation === false;
const allowDeactivateOnProfile = canChangeProfile && activation === true;

return (
<>
<Dropdown>
{({ closeDropdown, onToggleClick, open }) => {
this.closeDropdown = closeDropdown;
return (
<div className={classNames('pull-left dropdown', { open })}>
<button className="js-bulk-change" onClick={onToggleClick}>
{translate('bulk_change')}
</button>
<ul className="dropdown-menu">
<li>
<a href="#" onClick={this.handleActivateClick}>
{translate('coding_rules.activate_in')}…
</a>
</li>
{allowActivateOnProfile &&
profile && (
<li>
<a href="#" onClick={this.handleActivateInProfileClick}>
{translate('coding_rules.activate_in')} <strong>{profile.name}</strong>
</a>
</li>
)}
<li>
<a href="#" onClick={this.handleDeactivateClick}>
{translate('coding_rules.deactivate_in')}…
</a>
</li>
{allowDeactivateOnProfile &&
profile && (
<li>
<a href="#" onClick={this.handleDeactivateInProfileClick}>
{translate('coding_rules.deactivate_in')} <strong>{profile.name}</strong>
</a>
</li>
)}
</ul>
</div>
);
}}
</Dropdown>
{this.state.modal &&
this.state.action && (
<BulkChangeModal
action={this.state.action}
onClose={this.closeModal}
organization={this.props.organization}
profile={this.state.profile}
query={this.props.query}
referencedProfiles={this.props.referencedProfiles}
total={this.props.total}
/>
)}
</>
);
}
}

+ 256
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx View File

@@ -0,0 +1,256 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import * as classNames from 'classnames';
import { Query, serializeQuery } from '../query';
import { Profile, bulkActivateRules, bulkDeactivateRules } from '../../../api/quality-profiles';
import Modal from '../../../components/controls/Modal';
import Select from '../../../components/controls/Select';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';

interface Props {
action: string;
onClose: () => void;
organization: string | undefined;
referencedProfiles: { [profile: string]: Profile };
profile?: Profile;
query: Query;
total: number;
}

interface ActivationResult {
failed: number;
profile: string;
succeeded: number;
}

interface State {
finished: boolean;
results: ActivationResult[];
selectedProfiles: any[];
submitting: boolean;
}

export default class BulkChangeModal extends React.PureComponent<Props, State> {
mounted: boolean;

constructor(props: Props) {
super(props);

// if there is only one possible option for profile, select it immediately
const selectedProfiles = [];
const availableProfiles = this.getAvailableQualityProfiles(props);
if (availableProfiles.length === 1) {
selectedProfiles.push(availableProfiles[0].key);
}

this.state = { finished: false, results: [], selectedProfiles, submitting: false };
}

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleCloseClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onClose();
};

handleProfileSelect = (options: { value: string }[]) => {
const selectedProfiles = options.map(option => option.value);
this.setState({ selectedProfiles });
};

getAvailableQualityProfiles = ({ query, referencedProfiles } = this.props) => {
let profiles = Object.values(referencedProfiles);
if (query.languages.length > 0) {
profiles = profiles.filter(profile => query.languages.includes(profile.language));
}
return profiles
.filter(profile => profile.actions && profile.actions.edit)
.filter(profile => !profile.isBuiltIn);
};

processResponse = (profile: string, response: any) => {
if (this.mounted) {
const result: ActivationResult = {
failed: response.failed || 0,
profile,
succeeded: response.succeeded || 0
};
this.setState(state => ({ results: [...state.results, result] }));
}
};

sendRequests = () => {
let looper = Promise.resolve();

// serialize the query, but delete the `profile`
const data = serializeQuery(this.props.query);
delete data.profile;

const method = this.props.action === 'activate' ? bulkActivateRules : bulkDeactivateRules;

// if a profile is selected in the facet, pick it
// otherwise take all profiles selected in the dropdown
const profiles: string[] = this.props.profile
? [this.props.profile.key]
: this.state.selectedProfiles;

for (const profile of profiles) {
looper = looper.then(() =>
method({ ...data, organization: this.props.organization, targetKey: profile }).then(
response => this.processResponse(profile, response)
)
);
}
return looper;
};

handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.setState({ submitting: true });
this.sendRequests().then(
() => {
if (this.mounted) {
this.setState({ finished: true, submitting: false });
}
},
() => {
if (this.mounted) {
this.setState({ submitting: false });
}
}
);
};

renderResult = (result: ActivationResult) => {
const { profile: profileKey } = result;
const profile = this.props.referencedProfiles[profileKey];
if (!profile) {
return null;
}
return (
<div
className={classNames('alert', {
'alert-warning': result.failed > 0,
'alert-success': result.failed === 0
})}
key={result.profile}>
{result.failed
? translateWithParameters(
'coding_rules.bulk_change.warning',
profile.name,
profile.language,
result.succeeded,
result.failed
)
: translateWithParameters(
'coding_rules.bulk_change.success',
profile.name,
profile.language,
result.succeeded
)}
</div>
);
};

renderProfileSelect = () => {
const profiles = this.getAvailableQualityProfiles();
const options = profiles.map(profile => ({
label: `${profile.name} - ${profile.languageName}`,
value: profile.key
}));
return (
<Select
multi={true}
onChange={this.handleProfileSelect}
options={options}
value={this.state.selectedProfiles}
/>
);
};

render() {
const { action, profile, total } = this.props;
const header =
// prettier-ignore
action === 'activate'
? `${translate('coding_rules.activate_in_quality_profile')} (${formatMeasure(total, 'INT')} ${translate('coding_rules._rules')})`
: `${translate('coding_rules.deactivate_in_quality_profile')} (${formatMeasure(total, 'INT')} ${translate('coding_rules._rules')})`;

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<form onSubmit={this.handleFormSubmit}>
<header className="modal-head">
<h2>{header}</h2>
</header>

<div className="modal-body">
{this.state.results.map(this.renderResult)}

{!this.state.finished &&
!this.state.submitting && (
<div className="modal-field">
<h3>
<label htmlFor="coding-rules-bulk-change-profile">
{action === 'activate'
? translate('coding_rules.activate_in')
: translate('coding_rules.deactivate_in')}
</label>
</h3>
{profile ? (
<h3 className="readonly-field">
{profile.name}
{' — '}
{translate('are_you_sure')}
</h3>
) : (
this.renderProfileSelect()
)}
</div>
)}
</div>

<footer className="modal-foot">
{this.state.submitting && <i className="spinner spacer-right" />}
{!this.state.finished && (
<button
disabled={this.state.submitting}
id="coding-rules-submit-bulk-change"
type="submit">
{translate('apply')}
</button>
)}
<button className="button-link" onClick={this.handleCloseClick} type="reset">
{this.state.finished ? translate('close') : translate('cancel')}
</button>
</footer>
</form>
</Modal>
);
}
}

+ 0
- 94
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js View File

@@ -1,94 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { getAppState } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
import init from '../init';
import '../styles.css';

class CodingRulesAppContainer extends React.PureComponent {
/*:: stop: ?() => void; */
/*:: props: {
appState: {
defaultOrganization: string,
organizationsEnabled: boolean
},
params: {
organizationKey?: string
},
router: {
replace: string => void
}
};
*/

componentDidMount() {
// $FlowFixMe
document.body.classList.add('white-page');

if (this.props.appState.organizationsEnabled && !this.props.params.organizationKey) {
// redirect to organization-level rules page
this.props.router.replace(
'/organizations/' +
this.props.appState.defaultOrganization +
'/rules' +
window.location.hash
);
} else {
this.stop = init(
this.refs.container,
this.props.params.organizationKey,
this.props.params.organizationKey === this.props.appState.defaultOrganization
);
}
}

componentWillUnmount() {
// $FlowFixMe
document.body.classList.remove('white-page');

if (this.stop) {
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>
<Helmet title={translate('coding_rules.page')} />
<div ref="container" />
</div>
);
}
}

const mapStateToProps = state => ({
appState: getAppState(state)
});

export default connect(mapStateToProps)(withRouter(CodingRulesAppContainer));

+ 99
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx View File

@@ -0,0 +1,99 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import SimpleModal from '../../../components/controls/SimpleModal';
import { translate } from '../../../helpers/l10n';

interface Props {
children: (
props: { onClick: (event: React.SyntheticEvent<HTMLButtonElement>) => void }
) => React.ReactNode;
confirmButtonText: string;
confirmData?: string;
isDestructive?: boolean;
modalBody: React.ReactNode;
modalHeader: string;
onConfirm: (data?: string) => void | Promise<void>;
}

interface State {
modal: boolean;
}

// TODO move this component to components/ and use everywhere!
export default class ConfirmButton extends React.PureComponent<Props, State> {
state: State = { modal: false };

handleButtonClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ modal: true });
};

handleSubmit = () => {
const result = this.props.onConfirm(this.props.confirmData);
if (result) {
result.then(this.handleCloseModal, () => {});
} else {
this.handleCloseModal();
}
};

handleCloseModal = () => this.setState({ modal: false });

render() {
const { confirmButtonText, isDestructive, modalBody, modalHeader } = this.props;

return (
<>
{this.props.children({ onClick: this.handleButtonClick })}
{this.state.modal && (
<SimpleModal
header={modalHeader}
onClose={this.handleCloseModal}
onSubmit={this.handleSubmit}>
{({ onCloseClick, onSubmitClick, submitting }) => (
<>
<header className="modal-head">
<h2>{modalHeader}</h2>
</header>

<div className="modal-body">{modalBody}</div>

<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
<button
className={isDestructive ? 'button-red' : undefined}
disabled={submitting}
onClick={onSubmitClick}>
{confirmButtonText}
</button>
<a href="#" onClick={onCloseClick}>
{translate('cancel')}
</a>
</footer>
</>
)}
</SimpleModal>
)}
</>
);
}
}

+ 83
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.tsx View File

@@ -0,0 +1,83 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import CustomRuleFormModal from './CustomRuleFormModal';
import { RuleDetails } from '../../../app/types';

interface Props {
children: (
props: { onClick: (event: React.SyntheticEvent<HTMLButtonElement>) => void }
) => React.ReactNode;
customRule?: RuleDetails;
onDone: (newRuleDetails: RuleDetails) => void;
organization: string | undefined;
templateRule: RuleDetails;
}

interface State {
modal: boolean;
}

export default class CustomRuleButton extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { modal: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ modal: true });
};

handleModalClose = () => {
if (this.mounted) {
this.setState({ modal: false });
}
};

handleDone = (newRuleDetails: RuleDetails) => {
this.handleModalClose();
this.props.onDone(newRuleDetails);
};

render() {
return (
<>
{this.props.children({ onClick: this.handleClick })}
{this.state.modal && (
<CustomRuleFormModal
customRule={this.props.customRule}
onClose={this.handleModalClose}
onDone={this.handleDone}
organization={this.props.organization}
templateRule={this.props.templateRule}
/>
)}
</>
);
}
}

+ 415
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx View File

@@ -0,0 +1,415 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { RuleDetails, RuleParameter } from '../../../app/types';
import Modal from '../../../components/controls/Modal';
import { translate } from '../../../helpers/l10n';
import MarkdownTips from '../../../components/common/MarkdownTips';
import { SEVERITIES, TYPES, RULE_STATUSES } from '../../../helpers/constants';
import latinize from '../../../helpers/latinize';
import Select from '../../../components/controls/Select';
import TypeHelper from '../../../components/shared/TypeHelper';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { createRule, updateRule } from '../../../api/rules';
import { csvEscape } from '../../../helpers/csv';

interface Props {
customRule?: RuleDetails;
onClose: () => void;
onDone: (newRuleDetails: RuleDetails) => void;
organization: string | undefined;
templateRule: RuleDetails;
}

interface State {
description: string;
key: string;
keyModifiedByUser: boolean;
name: string;
params: { [p: string]: string };
reactivating: boolean;
severity: string;
status: string;
submitting: boolean;
type: string;
}

export default class CustomRuleFormModal extends React.PureComponent<Props, State> {
mounted: boolean;

constructor(props: Props) {
super(props);
const params: { [p: string]: string } = {};
if (props.customRule && props.customRule.params) {
for (const param of props.customRule.params) {
params[param.key] = param.defaultValue || '';
}
}
this.state = {
description: (props.customRule && props.customRule.mdDesc) || '',
key: '',
keyModifiedByUser: false,
name: (props.customRule && props.customRule.name) || '',
params,
reactivating: false,
severity: (props.customRule && props.customRule.severity) || props.templateRule.severity,
status: (props.customRule && props.customRule.status) || props.templateRule.status,
submitting: false,
type: (props.customRule && props.customRule.type) || props.templateRule.type
};
}

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleCancelClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onClose();
};

prepareRequest = () => {
/* eslint-disable camelcase */
const { customRule, organization, templateRule } = this.props;
const params = Object.keys(this.state.params)
.map(key => `${key}=${csvEscape(this.state.params[key])}`)
.join(';');
const ruleData = {
markdown_description: this.state.description,
name: this.state.name,
organization,
params,
severity: this.state.severity,
status: this.state.status
};
return customRule
? updateRule({ ...ruleData, key: customRule.key })
: createRule({
...ruleData,
custom_key: this.state.key,
prevent_reactivation: !this.state.reactivating,
template_key: templateRule.key,
type: this.state.type
});
/* eslint-enable camelcase */
};

handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.setState({ submitting: true });
this.prepareRequest().then(
newRuleDetails => {
if (this.mounted) {
this.setState({ submitting: false });
this.props.onDone(newRuleDetails);
}
},
(response: Response) => {
if (this.mounted) {
this.setState({ reactivating: response.status === 409, submitting: false });
}
}
);
};

handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const { value: name } = event.currentTarget;
this.setState((state: State) => {
const change: Partial<State> = { name };
if (!state.keyModifiedByUser) {
change.key = latinize(name).replace(/[^A-Za-z0-9]/g, '_');
}
return change;
});
};

handleKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ key: event.currentTarget.value, keyModifiedByUser: true });

handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) =>
this.setState({ description: event.currentTarget.value });

handleTypeChange = ({ value }: { value: string }) => this.setState({ type: value });

handleSeverityChange = ({ value }: { value: string }) => this.setState({ severity: value });

handleStatusChange = ({ value }: { value: string }) => this.setState({ status: value });

handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = event.currentTarget;
this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
};

renderNameField = () => (
<tr className="property">
<th className="nowrap">
<h3>
{translate('name')} <em className="mandatory">*</em>
</h3>
</th>
<td>
<input
autoFocus={true}
className="coding-rules-name-key"
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-name"
onChange={this.handleNameChange}
required={true}
type="text"
value={this.state.name}
/>
</td>
</tr>
);

renderKeyField = () => (
<tr className="property">
<th className="nowrap">
<h3>
{translate('key')} {!this.props.customRule && <em className="mandatory">*</em>}
</h3>
</th>
<td>
{this.props.customRule ? (
<span className="coding-rules-detail-custom-rule-key" title={this.props.customRule.key}>
{this.props.customRule.key}
</span>
) : (
<input
className="coding-rules-name-key"
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-key"
onChange={this.handleKeyChange}
required={true}
type="text"
value={this.state.key}
/>
)}
</td>
</tr>
);

renderDescriptionField = () => (
<tr className="property">
<th className="nowrap">
<h3>
{translate('description')} <em className="mandatory">*</em>
</h3>
</th>
<td>
<textarea
className="coding-rules-markdown-description"
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-html-description"
onChange={this.handleDescriptionChange}
required={true}
rows={5}
value={this.state.description}
/>
<span className="text-right">
<MarkdownTips />
</span>
</td>
</tr>
);

renderTypeOption = ({ value }: { value: string }) => <TypeHelper type={value} />;

renderTypeField = () => (
<tr className="property">
<th className="nowrap">
<h3>{translate('type')}</h3>
</th>
<td>
<Select
className="input-medium"
clearable={false}
disabled={this.state.submitting}
onChange={this.handleTypeChange}
options={TYPES.map(type => ({
label: translate('issue.type', type),
value: type
}))}
optionRenderer={this.renderTypeOption}
searchable={false}
value={this.state.type}
valueRenderer={this.renderTypeOption}
/>
</td>
</tr>
);

renderSeverityOption = ({ value }: { value: string }) => <SeverityHelper severity={value} />;

renderSeverityField = () => (
<tr className="property">
<th className="nowrap">
<h3>{translate('severity')}</h3>
</th>
<td>
<Select
className="input-medium"
clearable={false}
disabled={this.state.submitting}
onChange={this.handleSeverityChange}
options={SEVERITIES.map(severity => ({
label: translate('severity', severity),
value: severity
}))}
optionRenderer={this.renderSeverityOption}
searchable={false}
value={this.state.severity}
valueRenderer={this.renderSeverityOption}
/>
</td>
</tr>
);

renderStatusField = () => (
<tr className="property">
<th className="nowrap">
<h3>{translate('coding_rules.filters.status')}</h3>
</th>
<td>
<Select
className="input-medium"
clearable={false}
disabled={this.state.submitting}
onChange={this.handleStatusChange}
options={RULE_STATUSES.map(status => ({
label: translate('rules.status', status),
value: status
}))}
searchable={false}
value={this.state.status}
/>
</td>
</tr>
);

renderParameterField = (param: RuleParameter) => (
<tr className="property" key={param.key}>
<th className="nowrap">
<h3>{param.key}</h3>
</th>
<td>
{param.type === 'TEXT' ? (
<textarea
className="width100"
disabled={this.state.submitting}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
rows={3}
value={this.state.params[param.key] || ''}
/>
) : (
<input
className="input-super-large"
disabled={this.state.submitting}
name={param.key}
onChange={this.handleParameterChange}
placeholder={param.defaultValue}
type="text"
value={this.state.params[param.key] || ''}
/>
)}
<div className="note" dangerouslySetInnerHTML={{ __html: param.htmlDesc || '' }} />
{param.extra && <div className="note">{param.extra}</div>}
</td>
</tr>
);

renderSubmitButton = () => {
if (this.state.reactivating) {
return (
<button
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-reactivate"
type="submit">
{translate('coding_rules.reactivate')}
</button>
);
} else {
return (
<button
disabled={this.state.submitting}
id="coding-rules-custom-rule-creation-create"
type="submit">
{translate(this.props.customRule ? 'save' : 'create')}
</button>
);
}
};

render() {
const { customRule, templateRule } = this.props;
const { reactivating, submitting } = this.state;
const { params = [] } = templateRule;
const header = translate(
customRule ? 'coding_rules.update_custom_rule' : 'coding_rules.create_custom_rule'
);
return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<form onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>

<div className="modal-body modal-container">
{reactivating && (
<div className="alert alert-warning">{translate('coding_rules.reactivate.help')}</div>
)}
<table>
<tbody>
{this.renderNameField()}
{this.renderKeyField()}
{this.renderDescriptionField()}
{/* do not allow to change the type of existing rule */}
{!customRule && this.renderTypeField()}
{this.renderSeverityField()}
{this.renderStatusField()}
{params.map(this.renderParameterField)}
</tbody>
</table>
</div>

<div className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
{this.renderSubmitButton()}
<button
className="button-link"
disabled={submitting}
id="coding-rules-custom-rule-creation-cancel"
onClick={this.handleCancelClick}
type="reset">
{translate('cancel')}
</button>
</div>
</form>
</Modal>
);
}
}

server/sonar-web/src/main/js/apps/coding-rules/facets/status-facet.js → server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx View File

@@ -17,30 +17,27 @@
* 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 * as React from 'react';
import Facet, { BasicProps } from './Facet';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { SEVERITIES } from '../../../helpers/constants';
import { translate } from '../../../helpers/l10n';

export default BaseFacet.extend({
statuses: ['READY', 'DEPRECATED', 'BETA'],
export default class DefaultSeverityFacet extends React.PureComponent<BasicProps> {
renderName = (severity: string) => <SeverityHelper severity={severity} />;

getValues() {
const values = this.model.getValues();
return values.map(value => ({
...value,
label: translate('rules.status', value.val.toLowerCase())
}));
},
renderTextName = (severity: string) => translate('severity', severity);

sortValues(values) {
const order = this.statuses;
return sortBy(values, v => order.indexOf(v.val));
},

serializeData() {
return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
values: this.sortValues(this.getValues())
};
render() {
return (
<Facet
{...this.props}
halfWidth={true}
options={SEVERITIES}
property="severities"
renderName={this.renderName}
renderTextName={this.renderTextName}
/>
);
}
});
}

+ 123
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx View File

@@ -0,0 +1,123 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { orderBy, without, sortBy } from 'lodash';
import * as classNames from 'classnames';
import { FacetKey } from '../query';
import FacetBox from '../../../components/facet/FacetBox';
import FacetHeader from '../../../components/facet/FacetHeader';
import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';

export interface BasicProps {
onChange: (changes: { [x: string]: string | string[] | undefined }) => void;
onToggle: (facet: FacetKey) => void;
open: boolean;
stats?: { [x: string]: number };
values: string[];
}

interface Props extends BasicProps {
disabled?: boolean;
disabledHelper?: string;
halfWidth?: boolean;
options?: string[];
property: FacetKey;
renderFooter?: () => React.ReactNode;
renderName?: (value: string) => React.ReactNode;
renderTextName?: (value: string) => string;
singleSelection?: boolean;
}

export default class Facet extends React.PureComponent<Props> {
handleItemClick = (itemValue: string) => {
const { values } = this.props;
let newValue;
if (this.props.singleSelection) {
const value = values.length ? values[0] : undefined;
newValue = itemValue === value ? undefined : itemValue;
} else {
newValue = orderBy(
values.includes(itemValue) ? without(values, itemValue) : [...values, itemValue]
);
}
this.props.onChange({ [this.props.property]: newValue });
};

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

handleClear = () => this.props.onChange({ [this.props.property]: [] });

getStat = (value: string) => this.props.stats && this.props.stats[value];

renderItem = (value: string) => {
const active = this.props.values.includes(value);
const stat = this.getStat(value);
const { renderName = defaultRenderName } = this.props;

return (
<FacetItem
active={active}
disabled={stat === 0 && !active}
halfWidth={this.props.halfWidth}
key={value}
name={renderName(value)}
onClick={this.handleItemClick}
stat={stat && formatMeasure(stat, 'SHORT_INT')}
value={value}
/>
);
};

render() {
const { renderTextName = defaultRenderName, stats } = this.props;
const values = this.props.values.map(renderTextName);
const items =
this.props.options ||
(stats &&
sortBy(Object.keys(stats), key => -stats[key], key => renderTextName(key).toLowerCase()));

return (
<FacetBox
className={classNames({ 'search-navigator-facet-box-forbidden': this.props.disabled })}
property={this.props.property}>
<FacetHeader
helper={this.props.disabled ? this.props.disabledHelper : undefined}
name={translate('coding_rules.facet', this.props.property)}
onClear={this.handleClear}
onClick={this.props.disabled ? undefined : this.handleHeaderClick}
open={this.props.open && !this.props.disabled}
values={values}
/>

{this.props.open &&
items !== undefined && <FacetItemsList>{items.map(this.renderItem)}</FacetItemsList>}

{this.props.open && this.props.renderFooter !== undefined && this.props.renderFooter()}
</FacetBox>
);
}
}

function defaultRenderName(value: string) {
return value;
}

+ 146
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx View File

@@ -0,0 +1,146 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import ActivationSeverityFacet from './ActivationSeverityFacet';
import AvailableSinceFacet from './AvailableSinceFacet';
import DefaultSeverityFacet from './DefaultSeverityFacet';
import InheritanceFacet from './InheritanceFacet';
import LanguageFacet from './LanguageFacet';
import ProfileFacet from './ProfileFacet';
import RepositoryFacet from './RepositoryFacet';
import StatusFacet from './StatusFacet';
import TagFacet from './TagFacet';
import TemplateFacet from './TemplateFacet';
import TypeFacet from './TypeFacet';
import { Facets, Query, FacetKey, OpenFacets } from '../query';
import { Profile } from '../../../api/quality-profiles';

interface Props {
facets?: Facets;
onFacetToggle: (facet: FacetKey) => void;
onFilterChange: (changes: Partial<Query>) => void;
openFacets: OpenFacets;
organization: string | undefined;
organizationsEnabled?: boolean;
query: Query;
referencedProfiles: { [profile: string]: Profile };
referencedRepositories: { [repository: string]: { key: string; language: string; name: string } };
selectedProfile?: Profile;
}

export default function FacetsList(props: Props) {
const inheritanceDisabled =
props.query.compareToProfile !== undefined ||
props.selectedProfile === undefined ||
!props.selectedProfile.isInherited;

const activationSeverityDisabled =
props.query.compareToProfile !== undefined ||
props.selectedProfile === undefined ||
!props.query.activation;

return (
<div className="search-navigator-facets-list">
<LanguageFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.languages}
stats={props.facets && props.facets.languages}
values={props.query.languages}
/>
<TypeFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.types}
stats={props.facets && props.facets.types}
values={props.query.types}
/>
<TagFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
organization={props.organization}
open={!!props.openFacets.tags}
stats={props.facets && props.facets.tags}
values={props.query.tags}
/>
<RepositoryFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.repositories}
stats={props.facets && props.facets.repositories}
referencedRepositories={props.referencedRepositories}
values={props.query.repositories}
/>
<DefaultSeverityFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.severities}
stats={props.facets && props.facets.severities}
values={props.query.severities}
/>
<StatusFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.statuses}
stats={props.facets && props.facets.statuses}
values={props.query.statuses}
/>
<AvailableSinceFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.availableSince}
value={props.query.availableSince}
/>
{!props.organizationsEnabled && (
<TemplateFacet
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.template}
value={props.query.template}
/>
)}
<ProfileFacet
activation={props.query.activation}
compareToProfile={props.query.compareToProfile}
languages={props.query.languages}
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.profile}
referencedProfiles={props.referencedProfiles}
value={props.query.profile}
/>
<InheritanceFacet
disabled={inheritanceDisabled}
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.inheritance}
value={props.query.inheritance}
/>
<ActivationSeverityFacet
disabled={activationSeverityDisabled}
onChange={props.onFilterChange}
onToggle={props.onFacetToggle}
open={!!props.openFacets.activationSeverities}
stats={props.facets && props.facets.activationSeverities}
values={props.query.activationSeverities}
/>
</div>
);
}

+ 51
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/InheritanceFacet.tsx View File

@@ -0,0 +1,51 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import Facet, { BasicProps } from './Facet';
import { RuleInheritance, Omit } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

interface Props extends Omit<BasicProps, 'values'> {
disabled: boolean;
value: RuleInheritance | undefined;
}

export default class InheritanceFacet extends React.PureComponent<Props> {
renderName = (value: RuleInheritance) =>
translate('coding_rules.filters.inheritance', value.toLowerCase());

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

return (
<Facet
{...props}
disabled={this.props.disabled}
disabledHelper={translate('coding_rules.filters.inheritance.inactive')}
options={Object.values(RuleInheritance)}
property="inheritance"
renderName={this.renderName}
renderTextName={this.renderName}
singleSelection={true}
values={value ? [value] : []}
/>
);
}
}

+ 75
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx View File

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { connect } from 'react-redux';
import { uniq } from 'lodash';
import Facet, { BasicProps } from './Facet';
import LanguageFacetFooter from './LanguageFacetFooter';
import { getLanguages } from '../../../store/rootReducer';

interface StateProps {
referencedLanguages: { [language: string]: { key: string; name: string } };
}

interface Props extends BasicProps, StateProps {}

class LanguageFacet extends React.PureComponent<Props> {
getLanguageName = (language: string) => {
const { referencedLanguages } = this.props;
return referencedLanguages[language] ? referencedLanguages[language].name : language;
};

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

renderFooter = () => {
if (!this.props.stats) {
return null;
}

return (
<LanguageFacetFooter
onSelect={this.handleSelect}
referencedLanguages={this.props.referencedLanguages}
/>
);
};

render() {
const { referencedLanguages, ...facetProps } = this.props;
return (
<Facet
{...facetProps}
property="languages"
renderFooter={this.renderFooter}
renderName={this.getLanguageName}
renderTextName={this.getLanguageName}
/>
);
}
}

const mapStateToProps = (state: any): StateProps => ({
referencedLanguages: getLanguages(state)
});

export default connect(mapStateToProps)(LanguageFacet);

+ 54
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx View File

@@ -0,0 +1,54 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import Select from '../../../components/controls/Select';
import { translate } from '../../../helpers/l10n';

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

interface Props {
referencedLanguages: { [language: string]: { key: string; name: string } };
onSelect: (value: string) => void;
}

export default class LanguageFacetFooter extends React.PureComponent<Props> {
handleChange = (option: Option) => this.props.onSelect(option.value);

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

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

+ 71
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/PageActions.tsx View File

@@ -0,0 +1,71 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { Paging } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import PageCounter from '../../../components/common/PageCounter';
import ReloadButton from '../../../components/controls/ReloadButton';

interface Props {
loading: boolean;
onReload: () => void;
paging?: Paging;
selectedIndex?: number;
}

export default function PageActions(props: Props) {
return (
<div className="pull-right">
<Shortcuts />

<DeferredSpinner loading={props.loading}>
<ReloadButton onClick={props.onReload} />
</DeferredSpinner>

{props.paging && (
<PageCounter
className="spacer-left flash flash-heavy"
current={props.selectedIndex}
label={translate('coding_rules._rules')}
total={props.paging.total}
/>
)}
</div>
);
}

function Shortcuts() {
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('coding_rules.to_select_rules')}
</span>

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

+ 171
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx View File

@@ -0,0 +1,171 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { sortBy } from 'lodash';
import * as classNames from 'classnames';
import { Query, FacetKey } from '../query';
import { Profile } from '../../../api/quality-profiles';
import FacetBox from '../../../components/facet/FacetBox';
import FacetHeader from '../../../components/facet/FacetHeader';
import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';

interface Props {
activation: boolean | undefined;
compareToProfile: string | undefined;
languages: string[];
onChange: (changes: Partial<Query>) => void;
onToggle: (facet: FacetKey) => void;
open: boolean;
referencedProfiles: { [profile: string]: Profile };
value: string | undefined;
}

export default class ProfileFacet extends React.PureComponent<Props> {
handleItemClick = (selected: string) => {
const newValue = this.props.value === selected ? '' : selected;
this.props.onChange({
activation: this.props.activation === undefined ? true : this.props.activation,
compareToProfile: undefined,
profile: newValue
});
};

handleHeaderClick = () => this.props.onToggle('profile');

handleClear = () =>
this.props.onChange({
activation: undefined,
activationSeverities: [],
compareToProfile: undefined,
inheritance: undefined,
profile: undefined
});

handleActiveClick = (event: React.SyntheticEvent<HTMLElement>) => {
this.stopPropagation(event);
this.props.onChange({ activation: true, compareToProfile: undefined });
};

handleInactiveClick = (event: React.SyntheticEvent<HTMLElement>) => {
this.stopPropagation(event);
this.props.onChange({ activation: false, compareToProfile: undefined });
};

stopPropagation = (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
event.currentTarget.blur();
};

getTextValue = () => {
const { referencedProfiles, value } = this.props;
if (value) {
const profile = referencedProfiles[value];
const name = (profile && `${profile.name} ${profile.languageName}`) || value;
return [name];
} else {
return [];
}
};

renderName = (profile: Profile) => (
<>
{profile.name}
<span className="note little-spacer-left">
{profile.languageName}
{profile.isBuiltIn && ` (${translate('quality_profiles.built_in')})`}
</span>
</>
);

renderActivation = (profile: Profile) => {
const isCompare = profile.key === this.props.compareToProfile;
const activation = isCompare ? true : this.props.activation;
return (
<>
<span
aria-checked={activation}
className={classNames('js-active', 'facet-toggle', 'facet-toggle-green', {
'facet-toggle-active': activation
})}
onClick={isCompare ? this.stopPropagation : this.handleActiveClick}
role="radio"
tabIndex={-1}>
active
</span>
<span
aria-checked={!activation}
className={classNames('js-inactive', 'facet-toggle', 'facet-toggle-red', {
'facet-toggle-active': !activation
})}
onClick={isCompare ? this.stopPropagation : this.handleInactiveClick}
role="radio"
tabIndex={-1}>
inactive
</span>
</>
);
};

renderItem = (profile: Profile) => {
const active = [this.props.value, this.props.compareToProfile].includes(profile.key);

return (
<FacetItem
active={active}
className={this.props.compareToProfile === profile.key ? 'compare' : undefined}
key={profile.key}
name={this.renderName(profile)}
onClick={this.handleItemClick}
stat={this.renderActivation(profile)}
value={profile.key}
/>
);
};

render() {
const { languages, referencedProfiles } = this.props;
let profiles = Object.values(referencedProfiles);
if (languages.length > 0) {
profiles = profiles.filter(profile => languages.includes(profile.language));
}
profiles = sortBy(
profiles,
profile => profile.name.toLowerCase(),
profile => profile.languageName
);

return (
<FacetBox property="profile">
<FacetHeader
name={translate('coding_rules.facet.qprofile')}
onClear={this.handleClear}
onClick={this.handleHeaderClick}
open={this.props.open}
values={this.getTextValue()}
/>

{this.props.open && <FacetItemsList>{profiles.map(this.renderItem)}</FacetItemsList>}
</FacetBox>
);
}
}

+ 60
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx View File

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import SimpleModal from '../../../components/controls/SimpleModal';
import { translate } from '../../../helpers/l10n';

interface Props {
onCancel: () => void;
onSubmit: () => void;
}

export default function RemoveExtendedDescriptionModal({ onCancel, onSubmit }: Props) {
const header = translate('coding_rules.remove_extended_description');
return (
<SimpleModal header={header} onClose={onCancel} onSubmit={onSubmit}>
{({ onCloseClick, onSubmitClick, submitting }) => (
<>
<header className="modal-head">
<h2>{header}</h2>
</header>

<div className="modal-body">
{translate('coding_rules.remove_extended_description.confirm')}
</div>

<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
<button
className="button-red"
disabled={submitting}
id="coding-rules-detail-extend-description-remove-submit"
onClick={onSubmitClick}>
{translate('remove')}
</button>
<a href="#" onClick={onCloseClick}>
{translate('cancel')}
</a>
</footer>
</>
)}
</SimpleModal>
);
}

+ 76
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx View File

@@ -0,0 +1,76 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { connect } from 'react-redux';
import Facet, { BasicProps } from './Facet';
import { getLanguages } from '../../../store/rootReducer';

interface StateProps {
referencedLanguages: { [language: string]: { key: string; name: string } };
}

interface Props extends BasicProps, StateProps {
referencedRepositories: { [repository: string]: { key: string; language: string; name: string } };
}

class RepositoryFacet extends React.PureComponent<Props> {
getLanguageName = (languageKey: string) => {
const { referencedLanguages } = this.props;
const language = referencedLanguages[languageKey];
return (language && language.name) || languageKey;
};

renderName = (repositoryKey: string) => {
const { referencedRepositories } = this.props;
const repository = referencedRepositories[repositoryKey];
return repository ? (
<>
{repository.name}
<span className="note little-spacer-left">{this.getLanguageName(repository.language)}</span>
</>
) : (
repositoryKey
);
};

renderTextName = (repositoryKey: string) => {
const { referencedRepositories } = this.props;
const repository = referencedRepositories[repositoryKey];
return (repository && repository.name) || repositoryKey;
};

render() {
const { referencedLanguages, referencedRepositories, ...facetProps } = this.props;
return (
<Facet
{...facetProps}
property="repositories"
renderName={this.renderName}
renderTextName={this.renderTextName}
/>
);
}
}

const mapStateToProps = (state: any): StateProps => ({
referencedLanguages: getLanguages(state)
});

export default connect(mapStateToProps)(RepositoryFacet);

+ 242
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx View File

@@ -0,0 +1,242 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import ConfirmButton from './ConfirmButton';
import CustomRuleButton from './CustomRuleButton';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import RuleDetailsCustomRules from './RuleDetailsCustomRules';
import RuleDetailsDescription from './RuleDetailsDescription';
import RuleDetailsIssues from './RuleDetailsIssues';
import RuleDetailsMeta from './RuleDetailsMeta';
import RuleDetailsParameters from './RuleDetailsParameters';
import RuleDetailsProfiles from './RuleDetailsProfiles';
import { Query, Activation } from '../query';
import { Profile } from '../../../api/quality-profiles';
import { getRuleDetails, deleteRule, updateRule } from '../../../api/rules';
import { RuleActivation, RuleDetails as IRuleDetails } from '../../../app/types';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
allowCustomRules?: boolean;
canWrite?: boolean;
onActivate: (profile: string, rule: string, activation: Activation) => void;
onDeactivate: (profile: string, rule: string) => void;
onDelete: (rule: string) => void;
onFilterChange: (changes: Partial<Query>) => void;
organization: string | undefined;
referencedProfiles: { [profile: string]: Profile };
referencedRepositories: { [repository: string]: { key: string; language: string; name: string } };
ruleKey: string;
selectedProfile?: Profile;
}

interface State {
actives?: RuleActivation[];
loading: boolean;
ruleDetails?: IRuleDetails;
}

export default class RuleDetails extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
this.setState({ loading: true });
this.fetchRuleDetails();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.ruleKey !== this.props.ruleKey) {
this.setState({ loading: true });
this.fetchRuleDetails();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchRuleDetails = () =>
getRuleDetails({
actives: true,
key: this.props.ruleKey,
organization: this.props.organization
}).then(
({ actives, rule }) => {
if (this.mounted) {
this.setState({ actives, loading: false, ruleDetails: rule });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);

handleRuleChange = (ruleDetails: IRuleDetails) => {
if (this.mounted) {
this.setState({ ruleDetails });
}
};

handleTagsChange = (tags: string[]) => {
// optimistic update
const oldTags = this.state.ruleDetails && this.state.ruleDetails.tags;
this.setState(state => ({ ruleDetails: { ...state.ruleDetails, tags } }));
updateRule({
key: this.props.ruleKey,
organization: this.props.organization,
tags: tags.join()
}).catch(() => {
if (this.mounted) {
this.setState(state => ({ ruleDetails: { ...state.ruleDetails, tags: oldTags } }));
}
});
};

handleActivate = () =>
this.fetchRuleDetails().then(() => {
const { ruleKey, selectedProfile } = this.props;
if (selectedProfile && this.state.actives) {
const active = this.state.actives.find(active => active.qProfile === selectedProfile.key);
if (active) {
this.props.onActivate(selectedProfile.key, ruleKey, active);
}
}
});

handleDeactivate = () =>
this.fetchRuleDetails().then(() => {
const { ruleKey, selectedProfile } = this.props;
if (selectedProfile && this.state.actives) {
if (!this.state.actives.find(active => active.qProfile === selectedProfile.key)) {
this.props.onDeactivate(selectedProfile.key, ruleKey);
}
}
});

handleDelete = () =>
deleteRule({ key: this.props.ruleKey, organization: this.props.organization }).then(() =>
this.props.onDelete(this.props.ruleKey)
);

render() {
const { ruleDetails } = this.state;

if (!ruleDetails) {
return <div className="coding-rule-details" />;
}

const { allowCustomRules, canWrite, organization, referencedProfiles } = this.props;
const { params = [] } = ruleDetails;

const isCustom = !!ruleDetails.templateKey;
const isEditable = canWrite && !!this.props.allowCustomRules && isCustom;

return (
<div className="coding-rule-details">
<DeferredSpinner loading={this.state.loading}>
<RuleDetailsMeta
canWrite={canWrite}
onFilterChange={this.props.onFilterChange}
onTagsChange={this.handleTagsChange}
organization={organization}
referencedRepositories={this.props.referencedRepositories}
ruleDetails={ruleDetails}
/>

<RuleDetailsDescription
canWrite={canWrite}
onChange={this.handleRuleChange}
organization={organization}
ruleDetails={ruleDetails}
/>

{params.length > 0 && <RuleDetailsParameters params={params} />}

{isEditable && (
<div className="coding-rules-detail-description">
{/* `templateRule` is used to get rule meta data, `customRule` is used to get parameter values */}
{/* it's expected to pass the same rule to both parameters */}
<CustomRuleButton
customRule={ruleDetails}
onDone={this.handleRuleChange}
organization={organization}
templateRule={ruleDetails}>
{({ onClick }) => (
<button
className="js-edit-custom"
id="coding-rules-detail-custom-rule-change"
onClick={onClick}>
{translate('edit')}
</button>
)}
</CustomRuleButton>
<ConfirmButton
confirmButtonText={translate('delete')}
isDestructive={true}
modalBody={translateWithParameters(
'coding_rules.delete.custom.confirm',
ruleDetails.name
)}
modalHeader={translate('coding_rules.delete_rule')}
onConfirm={this.handleDelete}>
{({ onClick }) => (
<button
className="button-red spacer-left js-delete"
id="coding-rules-detail-rule-delete"
onClick={onClick}>
{translate('delete')}
</button>
)}
</ConfirmButton>
</div>
)}

{ruleDetails.isTemplate && (
<RuleDetailsCustomRules
canChange={allowCustomRules && canWrite}
organization={organization}
ruleDetails={ruleDetails}
/>
)}

{!ruleDetails.isTemplate && (
<RuleDetailsProfiles
activations={this.state.actives}
canWrite={canWrite}
onActivate={this.handleActivate}
onDeactivate={this.handleDeactivate}
organization={organization}
referencedProfiles={referencedProfiles}
ruleDetails={ruleDetails}
/>
)}

{!ruleDetails.isTemplate && (
<RuleDetailsIssues organization={organization} ruleKey={ruleDetails.key} />
)}
</DeferredSpinner>
</div>
);
}
}

+ 179
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx View File

@@ -0,0 +1,179 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { Link } from 'react-router';
import { sortBy } from 'lodash';
import ConfirmButton from './ConfirmButton';
import CustomRuleButton from './CustomRuleButton';
import { searchRules, deleteRule } from '../../../api/rules';
import { Rule, RuleDetails } from '../../../app/types';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getRuleUrl } from '../../../helpers/urls';

interface Props {
canChange?: boolean;
organization: string | undefined;
ruleDetails: RuleDetails;
}

interface State {
loading: boolean;
rules?: Rule[];
}

export default class RuleDetailsCustomRules extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
this.fetchRules();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.ruleDetails.key !== this.props.ruleDetails.key) {
this.fetchRules();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchRules = () => {
this.setState({ loading: true });
searchRules({
f: 'name,severity,params',
organization: this.props.organization,
/* eslint-disable camelcase */
template_key: this.props.ruleDetails.key
/* eslint-enable camelcase */
}).then(
({ rules }) => {
if (this.mounted) {
this.setState({ rules, loading: false });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

handleRuleCreate = (newRuleDetails: RuleDetails) => {
if (this.mounted) {
this.setState(({ rules = [] }: State) => ({
rules: [...rules, newRuleDetails]
}));
}
};

handleRuleDelete = (ruleKey: string) => {
return deleteRule({ key: ruleKey, organization: this.props.organization }).then(() => {
if (this.mounted) {
this.setState(({ rules = [] }) => ({
rules: rules.filter(rule => rule.key !== ruleKey)
}));
}
});
};

renderRule = (rule: Rule) => (
<tr key={rule.key} data-rule={rule.key}>
<td className="coding-rules-detail-list-name">
<Link to={getRuleUrl(rule.key, this.props.organization)}>{rule.name}</Link>
</td>

<td className="coding-rules-detail-list-severity">
<SeverityHelper severity={rule.severity} />
</td>

<td className="coding-rules-detail-list-parameters">
{rule.params &&
rule.params.filter(param => param.defaultValue).map(param => (
<div className="coding-rules-detail-list-parameter" key={param.key}>
<span className="key">{param.key}</span>
<span className="sep">:&nbsp;</span>
<span className="value" title={param.defaultValue}>
{param.defaultValue}
</span>
</div>
))}
</td>

{this.props.canChange && (
<td className="coding-rules-detail-list-actions">
<ConfirmButton
confirmButtonText={translate('delete')}
confirmData={rule.key}
isDestructive={true}
modalBody={translateWithParameters('coding_rules.delete.custom.confirm', rule.name)}
modalHeader={translate('coding_rules.delete_rule')}
onConfirm={this.handleRuleDelete}>
{({ onClick }) => (
<button className="button-red js-delete-custom-rule" onClick={onClick}>
{translate('delete')}
</button>
)}
</ConfirmButton>
</td>
)}
</tr>
);

render() {
const { loading, rules = [] } = this.state;

return (
<div className="js-rule-custom-rules coding-rule-section">
<div className="coding-rules-detail-custom-rules-section">
<div className="coding-rule-section-separator" />

<h3 className="coding-rules-detail-title">{translate('coding_rules.custom_rules')}</h3>

{this.props.canChange && (
<CustomRuleButton
onDone={this.handleRuleCreate}
organization={this.props.organization}
templateRule={this.props.ruleDetails}>
{({ onClick }) => (
<button className="js-create-custom-rule spacer-left" onClick={onClick}>
{translate('coding_rules.create')}
</button>
)}
</CustomRuleButton>
)}

<DeferredSpinner loading={loading}>
{rules.length > 0 && (
<table id="coding-rules-detail-custom-rules" className="coding-rules-detail-list">
<tbody>{sortBy(rules, rule => rule.name).map(this.renderRule)}</tbody>
</table>
)}
</DeferredSpinner>
</div>
</div>
);
}
}

+ 216
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx View File

@@ -0,0 +1,216 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
import { updateRule } from '../../../api/rules';
import { RuleDetails } from '../../../app/types';
import MarkdownTips from '../../../components/common/MarkdownTips';
import { translate } from '../../../helpers/l10n';

interface Props {
canWrite: boolean | undefined;
onChange: (newRuleDetails: RuleDetails) => void;
organization: string | undefined;
ruleDetails: RuleDetails;
}

interface State {
description: string;
descriptionForm: boolean;
removeDescriptionModal: boolean;
submitting: boolean;
}

export default class RuleDetailsDescription extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = {
description: '',
descriptionForm: false,
submitting: false,
removeDescriptionModal: false
};

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) =>
this.setState({ description: event.currentTarget.value });

handleCancelClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ descriptionForm: false });
};

handleSaveClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.updateDescription(this.state.description);
};

handleRemoveDescriptionClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ removeDescriptionModal: true });
};

handleCancelRemoving = () => this.setState({ removeDescriptionModal: false });

handleConfirmRemoving = () => {
this.setState({ removeDescriptionModal: false });
this.updateDescription('');
};

updateDescription = (text: string) => {
this.setState({ submitting: true });

updateRule({
key: this.props.ruleDetails.key,
/* eslint-disable camelcase */
markdown_note: text,
/* eslint-enable camelcase*/
organization: this.props.organization
}).then(
ruleDetails => {
this.props.onChange(ruleDetails);
if (this.mounted) {
this.setState({ submitting: false, descriptionForm: false });
}
},
() => {
if (this.mounted) {
this.setState({ submitting: false });
}
}
);
};

handleExtendDescriptionClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({
// set description` to the current `mdNote` each time the form is open
description: this.props.ruleDetails.mdNote || '',
descriptionForm: true
});
};

renderDescription = () => (
<div id="coding-rules-detail-description-extra">
{this.props.ruleDetails.htmlNote !== undefined && (
<div
className="rule-desc spacer-bottom markdown"
dangerouslySetInnerHTML={{ __html: this.props.ruleDetails.htmlNote }}
/>
)}
{this.props.canWrite && (
<button
id="coding-rules-detail-extend-description"
onClick={this.handleExtendDescriptionClick}>
{translate('coding_rules.extend_description')}
</button>
)}
</div>
);

renderForm = () => (
<div className="coding-rules-detail-extend-description-form">
<table className="width100">
<tbody>
<tr>
<td className="width100" colSpan={2}>
<textarea
autoFocus={true}
id="coding-rules-detail-extend-description-text"
onChange={this.handleDescriptionChange}
rows={4}
style={{ width: '100%', marginBottom: 4 }}
value={this.state.description}
/>
</td>
</tr>
<tr>
<td>
<button
disabled={this.state.submitting}
id="coding-rules-detail-extend-description-submit"
onClick={this.handleSaveClick}>
{translate('save')}
</button>
{this.props.ruleDetails.mdNote !== undefined && (
<>
<button
className="button-red spacer-left"
disabled={this.state.submitting}
id="coding-rules-detail-extend-description-remove"
onClick={this.handleRemoveDescriptionClick}>
{translate('remove')}
</button>
{this.state.removeDescriptionModal && (
<RemoveExtendedDescriptionModal
onCancel={this.handleCancelRemoving}
onSubmit={this.handleConfirmRemoving}
/>
)}
</>
)}
<button
className="spacer-left button-link"
disabled={this.state.submitting}
id="coding-rules-detail-extend-description-cancel"
onClick={this.handleCancelClick}>
{translate('cancel')}
</button>
{this.state.submitting && <i className="spinner spacer-left" />}
</td>
<td className="text-right">
<MarkdownTips />
</td>
</tr>
</tbody>
</table>
</div>
);

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

return (
<div className="js-rule-description">
<div
className="coding-rules-detail-description rule-desc markdown"
dangerouslySetInnerHTML={{ __html: ruleDetails.htmlDesc || '' }}
/>

{!ruleDetails.templateKey && (
<div className="coding-rules-detail-description coding-rules-detail-description-extra">
{!this.state.descriptionForm && this.renderDescription()}
{this.state.descriptionForm && this.props.canWrite && this.renderForm()}
</div>
)}
</div>
);
}
}

+ 159
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx View File

@@ -0,0 +1,159 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { Link } from 'react-router';
import { getFacet } from '../../../api/issues';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { translate } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { getIssuesUrl } from '../../../helpers/urls';

interface Props {
organization: string | undefined;
ruleKey: string;
}

interface Project {
count: number;
id: string;
key: string;
name: string;
}

interface State {
loading: boolean;
projects?: Project[];
total?: number;
}

export default class RuleDetailsIssues extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
this.fetchIssues();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.ruleKey !== this.props.ruleKey) {
this.fetchIssues();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchIssues = () => {
this.setState({ loading: true });
getFacet(
{ organization: this.props.organization, rules: this.props.ruleKey, resolved: false },
'projectUuids'
).then(
({ facet, response }) => {
if (this.mounted) {
const { components = [], paging } = response;
const projects = [];
for (const item of facet) {
const project = components.find(component => component.uuid === item.val);
if (project) {
projects.push({
count: item.count,
id: item.val,
key: project.key,
name: project.name
});
}
}
this.setState({ projects, loading: false, total: paging.total });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

renderTotal = () => {
const { total } = this.state;
if (total === undefined) {
return null;
}
const path = getIssuesUrl(
{ resolved: 'false', rules: this.props.ruleKey },
this.props.organization
);
return (
<>
{' ('}
<Link to={path}>{total}</Link>
{')'}
</>
);
};

renderProject = (project: Project) => {
const path = getIssuesUrl(
{ projectUuids: project.id, resolved: 'false', rules: this.props.ruleKey },
this.props.organization
);
return (
<tr key={project.key}>
<td className="coding-rules-detail-list-name">{project.name}</td>
<td className="coding-rules-detail-list-parameters">
<Link to={path}>{formatMeasure(project.count, 'INT')}</Link>
</td>
</tr>
);
};

render() {
const { loading, projects = [] } = this.state;

return (
<div className="js-rule-issues coding-rule-section">
<div className="coding-rule-section-separator" />

<DeferredSpinner loading={loading}>
<h3 className="coding-rules-detail-title">
{translate('coding_rules.issues')}
{this.renderTotal()}
</h3>

{projects.length > 0 && (
<table className="coding-rules-detail-list coding-rules-most-violated-projects">
<tbody>
<tr>
<td className="coding-rules-detail-list-name" colSpan={2}>
{translate('coding_rules.most_violating_projects')}
</td>
</tr>
{projects.map(this.renderProject)}
</tbody>
</table>
)}
</DeferredSpinner>
</div>
);
}
}

+ 236
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx View File

@@ -0,0 +1,236 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { Link } from 'react-router';
import { Query } from '../query';
import { RuleDetails } from '../../../app/types';
import { getRuleUrl } from '../../../helpers/urls';
import LinkIcon from '../../../components/icons-components/LinkIcon';
import SimilarRulesFilter from './SimilarRulesFilter';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import RuleDetailsTagsPopup from './RuleDetailsTagsPopup';
import TagsList from '../../../components/tags/TagsList';
import DateFormatter from '../../../components/intl/DateFormatter';

interface Props {
canWrite: boolean | undefined;
onFilterChange: (changes: Partial<Query>) => void;
onTagsChange: (tags: string[]) => void;
organization: string | undefined;
referencedRepositories: { [repository: string]: { key: string; language: string; name: string } };
ruleDetails: RuleDetails;
}

interface State {
tagsPopup: boolean;
}

export default class RuleDetailsMeta extends React.PureComponent<Props, State> {
state: State = { tagsPopup: false };

handleTagsClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState(state => ({ tagsPopup: !state.tagsPopup }));
};

handleTagsPopupToggle = (show: boolean) => this.setState({ tagsPopup: show });

renderType = () => {
const { ruleDetails } = this.props;
return (
<Tooltip overlay={translate('coding_rules.type.tooltip', ruleDetails.type)}>
<li className="coding-rules-detail-property" data-meta="type">
<IssueTypeIcon className="little-spacer-right" query={ruleDetails.type} />
{translate('issue.type', ruleDetails.type)}
</li>
</Tooltip>
);
};

renderSeverity = () => (
<Tooltip overlay={translate('default_severity')}>
<li className="coding-rules-detail-property" data-meta="severity">
<SeverityHelper severity={this.props.ruleDetails.severity} />
</li>
</Tooltip>
);

renderStatus = () => {
const { ruleDetails } = this.props;
if (ruleDetails.status === 'READY') {
return null;
}
return (
<Tooltip overlay={translate('status')}>
<li className="coding-rules-detail-property" data-meta="status">
<span className="badge badge-normal-size badge-danger-light">
{translate('rules.status', ruleDetails.status)}
</span>
</li>
</Tooltip>
);
};

renderTags = () => {
const { canWrite, ruleDetails } = this.props;
const { sysTags = [], tags = [] } = ruleDetails;
const allTags = [...sysTags, ...tags];
return (
<li className="coding-rules-detail-property" data-meta="tags">
{this.props.canWrite ? (
<BubblePopupHelper
isOpen={this.state.tagsPopup}
position="bottomleft"
popup={
<RuleDetailsTagsPopup
organization={this.props.organization}
setTags={this.props.onTagsChange}
sysTags={sysTags}
tags={tags}
/>
}
togglePopup={this.handleTagsPopupToggle}>
<button className="button-link" onClick={this.handleTagsClick}>
<TagsList
allowUpdate={canWrite}
tags={allTags.length > 0 ? allTags : [translate('coding_rules.no_tags')]}
/>
</button>
</BubblePopupHelper>
) : (
<TagsList
allowUpdate={canWrite}
tags={allTags.length > 0 ? allTags : [translate('coding_rules.no_tags')]}
/>
)}
</li>
);
};

renderCreationDate = () => (
<li className="coding-rules-detail-property" data-meta="available-since">
{translate('coding_rules.available_since')}{' '}
<DateFormatter date={this.props.ruleDetails.createdAt} />
</li>
);

renderRepository = () => {
const { referencedRepositories, ruleDetails } = this.props;
const repository = referencedRepositories[ruleDetails.repo];
if (!repository) {
return null;
}
return (
<Tooltip overlay={translate('coding_rules.repository_language')}>
<li className="coding-rules-detail-property" data-meta="repository">
{repository.name} ({ruleDetails.langName})
</li>
</Tooltip>
);
};

renderTemplate = () => {
if (!this.props.ruleDetails.isTemplate) {
return null;
}
return (
<Tooltip overlay={translate('coding_rules.rule_template.title')}>
<li className="coding-rules-detail-property">{translate('coding_rules.rule_template')}</li>
</Tooltip>
);
};

renderParentTemplate = () => {
const { ruleDetails } = this.props;
if (!ruleDetails.templateKey) {
return null;
}
return (
<Tooltip overlay={translate('coding_rules.custom_rule.title')}>
<li className="coding-rules-detail-property">
{translate('coding_rules.custom_rule')}
{' ('}
<Link to={getRuleUrl(ruleDetails.templateKey, this.props.organization)}>
{translate('coding_rules.show_template')}
</Link>
{')'}
</li>
</Tooltip>
);
};

renderRemediation = () => {
const { ruleDetails } = this.props;
if (!ruleDetails.debtRemFnType) {
return null;
}
return (
<Tooltip overlay={translate('coding_rules.remediation_function')}>
<li className="coding-rules-detail-property" data-meta="remediation-function">
{translate('coding_rules.remediation_function', ruleDetails.debtRemFnType)}
{':'}
{ruleDetails.debtRemFnOffset !== undefined && ` ${ruleDetails.debtRemFnOffset}`}
{ruleDetails.debtRemFnCoeff !== undefined && ` +${ruleDetails.debtRemFnCoeff}`}
{ruleDetails.effortToFixDescription !== undefined &&
` ${ruleDetails.effortToFixDescription}`}
</li>
</Tooltip>
);
};

render() {
const { ruleDetails } = this.props;
return (
<div className="js-rule-meta">
<header className="page-header">
<div className="pull-right">
<span className="note text-middle">{ruleDetails.key}</span>
<Link
className="coding-rules-detail-permalink link-no-underline spacer-left text-middle"
to={getRuleUrl(ruleDetails.key, this.props.organization)}>
<LinkIcon />
</Link>
<SimilarRulesFilter onFilterChange={this.props.onFilterChange} rule={ruleDetails} />
</div>
<h3 className="page-title coding-rules-detail-header">
<big>{ruleDetails.name}</big>
</h3>
</header>

<ul className="coding-rules-detail-properties">
{this.renderType()}
{this.renderSeverity()}
{this.renderStatus()}
{this.renderTags()}
{this.renderCreationDate()}
{this.renderRepository()}
{this.renderTemplate()}
{this.renderParentTemplate()}
{this.renderRemediation()}
</ul>
</div>
);
}
}

+ 55
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx View File

@@ -0,0 +1,55 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { RuleParameter } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

interface Props {
params: RuleParameter[];
}

export default class RuleDetailsParameters extends React.PureComponent<Props> {
renderParameter = (param: RuleParameter) => (
<tr className="coding-rules-detail-parameter" key={param.key}>
<td className="coding-rules-detail-parameter-name">{param.key}</td>
<td className="coding-rules-detail-parameter-description">
<p dangerouslySetInnerHTML={{ __html: param.htmlDesc || '' }} />
{param.defaultValue !== undefined && (
<div className="note spacer-top">
{translate('coding_rules.parameters.default_value')}
<br />
<span className="coding-rules-detail-parameter-value">{param.defaultValue}</span>
</div>
)}
</td>
</tr>
);

render() {
return (
<div className="js-rule-parameters">
<h3 className="coding-rules-detail-title">{translate('coding_rules.parameters')}</h3>
<table className="coding-rules-detail-parameters">
<tbody>{this.props.params.map(this.renderParameter)}</tbody>
</table>
</div>
);
}
}

+ 293
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx View File

@@ -0,0 +1,293 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { filter } from 'lodash';
import { Link } from 'react-router';
import ActivationButton from './ActivationButton';
import ConfirmButton from './ConfirmButton';
import RuleInheritanceIcon from './RuleInheritanceIcon';
import { Profile, deactivateRule, activateRule } from '../../../api/quality-profiles';
import { RuleActivation, RuleDetails, RuleInheritance } from '../../../app/types';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getQualityProfileUrl } from '../../../helpers/urls';
import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge';
import Tooltip from '../../../components/controls/Tooltip';
import SeverityHelper from '../../../components/shared/SeverityHelper';

interface Props {
activations: RuleActivation[] | undefined;
canWrite: boolean | undefined;
onActivate: () => Promise<void>;
onDeactivate: () => Promise<void>;
organization: string | undefined;
referencedProfiles: { [profile: string]: Profile };
ruleDetails: RuleDetails;
}

interface State {
loading: boolean;
}

export default class RuleDetailsProfiles extends React.PureComponent<Props, State> {
mounted: boolean;

componentDidMount() {
this.mounted = true;
this.fetchProfiles();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.ruleDetails.key !== this.props.ruleDetails.key) {
this.fetchProfiles();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchProfiles = () => this.setState({ loading: true });

handleActivate = () => this.props.onActivate();

handleDeactivate = (key?: string) => {
if (key) {
deactivateRule({
key,
organization: this.props.organization,
rule: this.props.ruleDetails.key
}).then(this.props.onDeactivate, () => {});
}
};

handleRevert = (key?: string) => {
if (key) {
activateRule({
key,
organization: this.props.organization,
rule: this.props.ruleDetails.key,
reset: true
}).then(this.props.onActivate, () => {});
}
};

renderInheritedProfile = (activation: RuleActivation, profile: Profile) => {
if (!profile.parentName) {
return null;
}
const profilePath = getQualityProfileUrl(
profile.parentName,
profile.language,
this.props.organization
);
return (
<div className="coding-rules-detail-quality-profile-inheritance">
{(activation.inherit === RuleInheritance.Overridden ||
activation.inherit === RuleInheritance.Inherited) && (
<>
<RuleInheritanceIcon
inheritance={activation.inherit}
parentProfileName={profile.parentName}
profileName={profile.name}
/>
<Link className="link-base-color spacer-left" to={profilePath}>
{profile.parentName}
</Link>
</>
)}
</div>
);
};

renderSeverity = (activation: RuleActivation, parentActivation?: RuleActivation) => (
<td className="coding-rules-detail-quality-profile-severity">
<Tooltip overlay={translate('coding_rules.activation_severity')}>
<span>
<SeverityHelper severity={activation.severity} />
</span>
</Tooltip>
{parentActivation !== undefined &&
activation.severity !== parentActivation.severity && (
<div className="coding-rules-detail-quality-profile-inheritance">
{translate('coding_rules.original')} {translate('severity', parentActivation.severity)}
</div>
)}
</td>
);

renderParameter = (param: { key: string; value: string }, parentActivation?: RuleActivation) => {
const originalParam =
parentActivation && parentActivation.params.find(p => p.key === param.key);
const originalValue = originalParam && originalParam.value;

return (
<div className="coding-rules-detail-quality-profile-parameter" key={param.key}>
<span className="key">{param.key}</span>
<span className="sep">{': '}</span>
<span className="value" title={param.value}>
{param.value}
</span>
{parentActivation &&
param.value !== originalValue && (
<div className="coding-rules-detail-quality-profile-inheritance">
{translate('coding_rules.original')} <span className="value">{originalValue}</span>
</div>
)}
</div>
);
};

renderParameters = (activation: RuleActivation, parentActivation?: RuleActivation) => (
<td className="coding-rules-detail-quality-profile-parameters">
{activation.params.map(param => this.renderParameter(param, parentActivation))}
</td>
);

renderActions = (activation: RuleActivation, profile: Profile) => {
const canEdit = profile.actions && profile.actions.edit && !profile.isBuiltIn;
const { ruleDetails } = this.props;
const hasParent = activation.inherit !== RuleInheritance.NotInherited && profile.parentKey;
return (
<td className="coding-rules-detail-quality-profile-actions">
{canEdit && (
<>
{!ruleDetails.isTemplate && (
<ActivationButton
activation={activation}
buttonText={translate('change_verb')}
className="coding-rules-detail-quality-profile-change"
modalHeader={translate('coding_rules.change_details')}
onDone={this.handleActivate}
organization={this.props.organization}
profiles={[profile]}
rule={ruleDetails}
/>
)}
{hasParent ? (
activation.inherit === RuleInheritance.Overridden &&
profile.parentName && (
<ConfirmButton
confirmButtonText={translate('yes')}
confirmData={profile.key}
modalBody={translateWithParameters(
'coding_rules.revert_to_parent_definition.confirm',
profile.parentName
)}
modalHeader={translate('coding_rules.revert_to_parent_definition')}
onConfirm={this.handleRevert}>
{({ onClick }) => (
<button
className="coding-rules-detail-quality-profile-revert button-red spacer-left"
onClick={onClick}>
{translate('coding_rules.revert_to_parent_definition')}
</button>
)}
</ConfirmButton>
)
) : (
<ConfirmButton
confirmButtonText={translate('yes')}
confirmData={profile.key}
modalBody={translate('coding_rules.deactivate.confirm')}
modalHeader={translate('coding_rules.deactivate')}
onConfirm={this.handleDeactivate}>
{({ onClick }) => (
<button
className="coding-rules-detail-quality-profile-deactivate button-red spacer-left"
onClick={onClick}>
{translate('coding_rules.deactivate')}
</button>
)}
</ConfirmButton>
)}
</>
)}
</td>
);
};

renderActivation = (activation: RuleActivation) => {
const { activations = [], ruleDetails } = this.props;
const profile = this.props.referencedProfiles[activation.qProfile];
if (!profile) {
return null;
}

const parentActivation = activations.find(x => x.qProfile === profile.parentKey);

return (
<tr key={profile.key} data-profile={profile.key}>
<td className="coding-rules-detail-quality-profile-name">
<Link to={getQualityProfileUrl(profile.name, profile.language, this.props.organization)}>
{profile.name}
</Link>
{profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />}
{this.renderInheritedProfile(activation, profile)}
</td>

{this.renderSeverity(activation, parentActivation)}
{!ruleDetails.templateKey && this.renderParameters(activation, parentActivation)}
{this.renderActions(activation, profile)}
</tr>
);
};

render() {
const { activations = [], referencedProfiles, ruleDetails } = this.props;
const canActivate = Object.values(referencedProfiles).some(profile =>
Boolean(profile.actions && profile.actions.edit && profile.language === ruleDetails.lang)
);

return (
<div className="js-rule-profiles coding-rule-section">
<div className="coding-rules-detail-quality-profiles-section">
<div className="coding-rule-section-separator" />

<h3 className="coding-rules-detail-title">
{translate('coding_rules.quality_profiles')}
</h3>

{canActivate && (
<ActivationButton
buttonText={translate('coding_rules.activate')}
className="coding-rules-quality-profile-activate spacer-left"
modalHeader={translate('coding_rules.activate_in_quality_profile')}
onDone={this.handleActivate}
organization={this.props.organization}
profiles={filter(
this.props.referencedProfiles,
profile => !activations.find(activation => activation.qProfile === profile.key)
)}
rule={ruleDetails}
/>
)}

{activations.length > 0 && (
<table
id="coding-rules-detail-quality-profiles"
className="coding-rules-detail-quality-profiles width100">
<tbody>{activations.map(this.renderActivation)}</tbody>
</table>
)}
</div>
</div>
);
}
}

+ 90
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx View File

@@ -0,0 +1,90 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { without, uniq } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { getRuleTags } from '../../../api/rules';
import { BubblePopupPosition } from '../../../components/common/BubblePopup';

interface Props {
organization: string | undefined;
popupPosition?: BubblePopupPosition;
setTags: (tags: string[]) => void;
sysTags: string[];
tags: string[];
}

interface State {
searchResult: any[];
}

const LIST_SIZE = 10;

export default class RuleDetailsTagsPopup extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { searchResult: [] };

componentDidMount() {
this.mounted = true;
this.onSearch('');
}

componentWillUnmount() {
this.mounted = false;
}

onSearch = (query: string) => {
getRuleTags({
q: query,
ps: Math.min(this.props.tags.length + LIST_SIZE, 100),
organization: this.props.organization
}).then(
tags => {
if (this.mounted) {
// systems tags can not be unset, don't display them in the results
this.setState({ searchResult: without(tags, ...this.props.sysTags) });
}
},
() => {}
);
};

onSelect = (tag: string) => {
this.props.setTags(uniq([...this.props.tags, tag]));
};

onUnselect = (tag: string) => {
this.props.setTags(without(this.props.tags, tag));
};

render() {
return (
<TagsSelector
position={this.props.popupPosition || {}}
tags={this.state.searchResult}
selectedTags={this.props.tags}
listSize={LIST_SIZE}
onSearch={this.onSearch}
onSelect={this.onSelect}
onUnselect={this.onUnselect}
/>
);
}
}

+ 44
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleInheritanceIcon.tsx View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import * as classNames from 'classnames';
import { RuleInheritance } from '../../../app/types';
import { translateWithParameters } from '../../../helpers/l10n';

interface Props {
inheritance: RuleInheritance.Inherited | RuleInheritance.Overridden;
parentProfileName: string;
profileName: string;
}

export default function RuleInheritanceIcon(props: Props) {
return (
<i
className={classNames('icon-inheritance', {
'icon-inheritance-overridden': props.inheritance === RuleInheritance.Overridden
})}
title={translateWithParameters(
'coding_rules.overrides',
props.profileName,
props.parentProfileName
)}
/>
);
}

+ 216
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx View File

@@ -0,0 +1,216 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import * as classNames from 'classnames';
import { Link } from 'react-router';
import { Activation, Query } from '../query';
import ActivationButton from './ActivationButton';
import ConfirmButton from './ConfirmButton';
import SimilarRulesFilter from './SimilarRulesFilter';
import { Profile, deactivateRule } from '../../../api/quality-profiles';
import { Rule, RuleInheritance } from '../../../app/types';
import Tooltip from '../../../components/controls/Tooltip';
import SeverityIcon from '../../../components/shared/SeverityIcon';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
activation?: Activation;
onActivate: (profile: string, rule: string, activation: Activation) => void;
onDeactivate: (profile: string, rule: string) => void;
onFilterChange: (changes: Partial<Query>) => void;
organization: string | undefined;
path: { pathname: string; query: { [x: string]: any } };
rule: Rule;
selected: boolean;
selectedProfile?: Profile;
}

export default class RuleListItem extends React.PureComponent<Props> {
handleDeactivate = () => {
if (this.props.selectedProfile) {
const data = {
key: this.props.selectedProfile.key,
organization: this.props.organization,
rule: this.props.rule.key
};
deactivateRule(data).then(() => this.props.onDeactivate(data.key, data.rule), () => {});
}
};

handleActivate = (severity: string) => {
if (this.props.selectedProfile) {
this.props.onActivate(this.props.selectedProfile.key, this.props.rule.key, {
severity,
inherit: RuleInheritance.NotInherited
});
}
return Promise.resolve();
};

renderActivation = () => {
const { activation, selectedProfile } = this.props;
if (!activation) {
return null;
}

return (
<td className="coding-rule-table-meta-cell coding-rule-activation">
<SeverityIcon severity={activation.severity} />
{selectedProfile &&
selectedProfile.parentName && (
<>
{activation.inherit === RuleInheritance.Overridden && (
<Tooltip
overlay={translateWithParameters(
'coding_rules.overrides',
selectedProfile.name,
selectedProfile.parentName
)}>
<i className="little-spacer-left icon-inheritance icon-inheritance-overridden" />
</Tooltip>
)}
{activation.inherit === RuleInheritance.Inherited && (
<Tooltip
overlay={translateWithParameters(
'coding_rules.inherits',
selectedProfile.name,
selectedProfile.parentName
)}>
<i className="little-spacer-left icon-inheritance" />
</Tooltip>
)}
</>
)}
</td>
);
};

renderActions = () => {
const { activation, rule, selectedProfile } = this.props;
if (!selectedProfile) {
return null;
}

const canEdit = selectedProfile.actions && selectedProfile.actions.edit;
if (!canEdit || selectedProfile.isBuiltIn) {
return null;
}

return (
<td className="coding-rule-table-meta-cell coding-rule-activation-actions">
{activation
? this.renderDeactivateButton(activation.inherit)
: !rule.isTemplate && (
<ActivationButton
buttonText={translate('coding_rules.activate')}
className="coding-rules-detail-quality-profile-activate"
modalHeader={translate('coding_rules.activate_in_quality_profile')}
onDone={this.handleActivate}
organization={this.props.organization}
profiles={[selectedProfile]}
rule={rule}
/>
)}
</td>
);
};

renderDeactivateButton = (inherit: string) => {
return inherit === 'NONE' ? (
<ConfirmButton
confirmButtonText={translate('yes')}
modalBody={translate('coding_rules.deactivate.confirm')}
modalHeader={translate('coding_rules.deactivate')}
onConfirm={this.handleDeactivate}>
{({ onClick }) => (
<button
className="coding-rules-detail-quality-profile-deactivate button-red"
onClick={onClick}>
{translate('coding_rules.deactivate')}
</button>
)}
</ConfirmButton>
) : (
<Tooltip overlay={translate('coding_rules.can_not_deactivate')} placement="left">
<button className="coding-rules-detail-quality-profile-deactivate button-red disabled">
{translate('coding_rules.deactivate')}
</button>
</Tooltip>
);
};

render() {
const { rule, selected } = this.props;
const allTags = [...(rule.tags || []), ...(rule.sysTags || [])];
return (
<div className={classNames('coding-rule', { selected })} data-rule={rule.key}>
<table className="coding-rule-table">
<tbody>
<tr>
{this.renderActivation()}

<td>
<div className="coding-rule-title">
<Link className="link-no-underline" to={this.props.path}>
{rule.name}
</Link>
{rule.isTemplate && (
<Tooltip overlay={translate('coding_rules.rule_template.title')}>
<span className="outline-badge spacer-left">
{translate('coding_rules.rule_template')}
</span>
</Tooltip>
)}
</div>
</td>

<td className="coding-rule-table-meta-cell">
<div className="coding-rule-meta">
{rule.status !== 'READY' && (
<span className="spacer-left badge badge-normal-size badge-danger-light">
{translate('rules.status', rule.status)}
</span>
)}
<span className="spacer-left note">{rule.langName}</span>
<Tooltip overlay={translate('coding_rules.type.tooltip', rule.type)}>
<span className="spacer-left note">
<IssueTypeIcon className="little-spacer-right" query={rule.type} />
{translate('issue.type', rule.type)}
</span>
</Tooltip>
{allTags.length > 0 && (
<span className="spacer-left">
<i className="icon-tags little-spacer-right" />
<span className="note">{allTags.join(', ')}</span>
</span>
)}
<SimilarRulesFilter onFilterChange={this.props.onFilterChange} rule={rule} />
</div>
</td>

{this.renderActions()}
</tr>
</tbody>
</table>
</div>
);
}
}

+ 133
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx View File

@@ -0,0 +1,133 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import * as classNames from 'classnames';
import { Query } from '../query';
import { Rule } from '../../../app/types';
import Dropdown from '../../../components/controls/Dropdown';
import { translate } from '../../../helpers/l10n';
import SeverityHelper from '../../../components/shared/SeverityHelper';

interface Props {
onFilterChange: (changes: Partial<Query>) => void;
rule: Rule;
}

export default class SimilarRulesFilter extends React.PureComponent<Props> {
closeDropdown: () => void;

handleLanguageClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
this.props.onFilterChange({ languages: [this.props.rule.lang] });
};

handleTypeClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
this.props.onFilterChange({ types: [this.props.rule.type] });
};

handleSeverityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
if (this.props.rule.severity) {
this.props.onFilterChange({ severities: [this.props.rule.severity] });
}
};

handleTagClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeDropdown();
const { tag } = event.currentTarget.dataset;
if (tag) {
this.props.onFilterChange({ tags: [tag] });
}
};

render() {
const { rule } = this.props;
const { tags = [], sysTags = [], severity } = rule;
const allTags = [...tags, ...sysTags];

return (
<Dropdown>
{({ closeDropdown, onToggleClick, open }) => {
this.closeDropdown = closeDropdown;
return (
<div className={classNames('dropdown display-inline-block', { open })}>
<a
className="js-rule-filter link-no-underline spacer-left dropdown-toggle"
href="#"
onClick={onToggleClick}>
<i className="icon-filter icon-half-transparent" />
<i className="icon-dropdown little-spacer-left" />
</a>
<div className="dropdown-menu dropdown-menu-right">
<header className="dropdown-header">
{translate('coding_rules.filter_similar_rules')}
</header>
<ul className="menu">
<li>
<a data-field="language" href="#" onClick={this.handleLanguageClick}>
{rule.langName}
</a>
</li>

<li>
<a data-field="type" href="#" onClick={this.handleTypeClick}>
{translate('issue.type', rule.type)}
</a>
</li>

{severity && (
<li>
<a data-field="severity" href="#" onClick={this.handleSeverityClick}>
<SeverityHelper severity={rule.severity} />
</a>
</li>
)}

{allTags.length > 0 && (
<>
<li className="divider" />
{allTags.map(tag => (
<li key={tag}>
<a data-field="tag" data-tag={tag} href="#" onClick={this.handleTagClick}>
<i className="icon-tags icon-half-transparent little-spacer-right" />
{tag}
</a>
</li>
))}
</>
)}
</ul>
</div>
</div>
);
}}
</Dropdown>
);
}
}

server/sonar-web/src/main/js/components/facet/FacetFooter.js → server/sonar-web/src/main/js/apps/coding-rules/components/StatusFacet.tsx View File

@@ -17,31 +17,23 @@
* 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 SearchSelect from '../controls/SearchSelect';
import * as React from 'react';
import Facet, { BasicProps } from './Facet';
import { RULE_STATUSES } from '../../../helpers/constants';
import { translate } 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<*>
|};
*/

export default class FacetFooter extends React.PureComponent {
/*:: props: Props; */
export default class StatusFacet extends React.PureComponent<BasicProps> {
renderName = (status: string) => translate('rules.status', status.toLowerCase());

render() {
return (
<div className="search-navigator-facet-footer">
<SearchSelect autofocus={false} {...this.props} />
</div>
<Facet
{...this.props}
options={RULE_STATUSES}
property="statuses"
renderName={this.renderName}
renderTextName={this.renderName}
/>
);
}
}

+ 64
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx View File

@@ -0,0 +1,64 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { uniq } from 'lodash';
import Facet, { BasicProps } from './Facet';
import { getRuleTags } from '../../../api/rules';
import FacetFooter from '../../../components/facet/FacetFooter';

interface Props extends BasicProps {
organization: string | undefined;
}

export default class TagFacet extends React.PureComponent<Props> {
handleSearch = (query: string) =>
getRuleTags({ organization: this.props.organization, ps: 50, q: query }).then(tags =>
tags.map(tag => ({ label: tag, value: tag }))
);

handleSelect = (tag: string) => this.props.onChange({ tags: uniq([...this.props.values, tag]) });

renderName = (tag: string) => (
<>
<i className="icon-tags icon-gray little-spacer-right" />
{tag}
</>
);

renderFooter = () => {
if (!this.props.stats) {
return null;
}

return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />;
};

render() {
const { organization, ...facetProps } = this.props;
return (
<Facet
{...facetProps}
property="tags"
renderFooter={this.renderFooter}
renderName={this.renderName}
/>
);
}
}

+ 62
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx View File

@@ -0,0 +1,62 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import Facet, { BasicProps } from './Facet';
import { Omit } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

interface Props extends Omit<BasicProps, 'onChange' | 'values'> {
onChange: (changes: { template: boolean | undefined }) => void;
value: boolean | undefined;
}

export default class TemplateFacet extends React.PureComponent<Props> {
handleChange = (changes: { template: string | any[] }) => {
const template =
// empty array is returned when a user cleared the facet
// otherwise `"true"`, `"false"` or `undefined` can be returned
Array.isArray(changes.template) || changes.template === undefined
? undefined
: changes.template === 'true';
this.props.onChange({ ...changes, template });
};

renderName = (template: string) =>
template === 'true'
? translate('coding_rules.filters.template.is_template')
: translate('coding_rules.filters.template.is_not_template');

render() {
const { onChange, value, ...props } = this.props;

return (
<Facet
{...props}
onChange={this.handleChange}
options={['true', 'false']}
property="template"
renderName={this.renderName}
renderTextName={this.renderName}
singleSelection={true}
values={value !== undefined ? [String(value)] : []}
/>
);
}
}

server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js → server/sonar-web/src/main/js/apps/coding-rules/components/TypeFacet.tsx View File

@@ -17,12 +17,32 @@
* 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 CodingRulesAppContainer from '../../coding-rules/components/CodingRulesAppContainer';
import * as React from 'react';
import Facet, { BasicProps } from './Facet';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { translate } from '../../../helpers/l10n';

export default class TypeFacet extends React.PureComponent<BasicProps> {
renderName = (type: string) => (
<>
<IssueTypeIcon className="little-spacer-right" query={type} />
{translate('issue.type', type)}
</>
);

renderTextName = (type: string) => translate('issue.type', type);

export default class OrganizationRules extends React.PureComponent {
render() {
return <CodingRulesAppContainer {...this.props} />;
const options = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];

return (
<Facet
{...this.props}
options={options}
property="types"
renderName={this.renderName}
renderTextName={this.renderTextName}
/>
);
}
}

+ 0
- 69
server/sonar-web/src/main/js/apps/coding-rules/confirm-dialog.js View File

@@ -1,69 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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';

const DEFAULTS = {
title: 'Confirmation',
html: '',
yesLabel: 'Yes',
noLabel: 'Cancel',
yesHandler() {
// no op
},
noHandler() {
// no op
},
always() {
// no op
}
};

export default function(options) {
const settings = { ...DEFAULTS, ...options };
const dialog = $(
'<div><div class="modal-head"><h2>' +
settings.title +
'</h2></div><div class="modal-body">' +
settings.html +
'</div><div class="modal-foot"><button data-confirm="yes">' +
settings.yesLabel +
'</button> <a data-confirm="no" class="action">' +
settings.noLabel +
'</a></div></div>'
);

$('[data-confirm=yes]', dialog).on('click', () => {
dialog.dialog('close');
settings.yesHandler();
return settings.always();
});

$('[data-confirm=no]', dialog).on('click', () => {
dialog.dialog('close');
settings.noHandler();
return settings.always();
});

return dialog.dialog({
modal: true,
minHeight: null,
width: 540
});
}

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

@@ -1,200 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 key from 'keymaster';
import Controller from '../../components/navigator/controller';
import Rule from './models/rule';
import RuleDetailsView from './rule-details-view';
import { searchRules, getRuleDetails } from '../../api/rules';

export default Controller.extend({
pageSize: 200,
ruleFields: [
'name',
'lang',
'langName',
'sysTags',
'tags',
'status',
'severity',
'isTemplate',
'templateKey'
],

_searchParameters() {
const fields = this.ruleFields.slice();
const profile = this.app.state.get('query').qprofile;
if (profile != null) {
fields.push('actives');
fields.push('params');
fields.push('isTemplate');
fields.push('severity');
}
const params = {
p: this.app.state.get('page'),
ps: this.pageSize,
facets: this._facetsFromServer().join(),
f: fields.join()
};
if (this.app.state.get('query').q == null) {
Object.assign(params, { s: 'name', asc: true });
}
return params;
},

fetchList(firstPage) {
firstPage = firstPage == null ? true : firstPage;
if (firstPage) {
this.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true });
}

this.hideDetails(firstPage);

const options = { ...this._searchParameters(), ...this.app.state.get('query') };
return searchRules(options).then(
r => {
const rules = this.app.list.parseRules(r);
if (firstPage) {
this.app.list.reset(rules);
} else {
this.app.list.add(rules);
}
this.app.list.setIndex();
this.app.list.addExtraAttributes(this.app.repositories);
this.app.facets.reset(this._allFacets());
this.app.facets.add(r.facets, { merge: true });
this.enableFacets(this._enabledFacets());
this.app.state.set({
page: r.p,
pageSize: r.ps,
total: r.total,
maxResultsReached: r.p * r.ps >= r.total
});
if (firstPage && this.isRulePermalink()) {
this.showDetails(this.app.list.first());
}
},
() => {
this.app.state.set({ maxResultsReached: true });
}
);
},

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

requestFacet(id) {
const facet = this.app.facets.get(id);
const options = { facets: id, ps: 1, ...this.app.state.get('query') };
return searchRules(options).then(r => {
const facetData = r.facets.find(facet => facet.property === id);
if (facetData) {
facet.set(facetData);
}
});
},

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

getRuleDetails(rule) {
const parameters = { key: rule.id, actives: true, organization: this.app.organization };
return getRuleDetails(parameters).then(r => {
rule.set(r.rule);
rule.addExtraAttributes(this.app.repositories);
return r;
});
},

showDetails(rule) {
const that = this;
const ruleModel = typeof rule === 'string' ? new Rule({ key: rule }) : rule;
this.app.layout.workspaceDetailsRegion.reset();
this.getRuleDetails(ruleModel).then(
r => {
key.setScope('details');
that.app.workspaceListView.unbindScrollEvents();
that.app.state.set({ rule: ruleModel });
that.app.workspaceDetailsView = new RuleDetailsView({
app: that.app,
model: ruleModel,
actives: r.actives
});
that.app.layout.showDetails();
that.app.layout.workspaceDetailsRegion.show(that.app.workspaceDetailsView);
},
() => {}
);
},

showDetailsForSelected() {
const rule = this.app.list.at(this.app.state.get('selectedIndex'));
this.showDetails(rule);
},

hideDetails(firstPage) {
key.setScope('list');
this.app.state.unset('rule');
this.app.layout.workspaceDetailsRegion.reset();
this.app.layout.hideDetails();
this.app.workspaceListView.bindScrollEvents();
if (firstPage) {
this.app.workspaceListView.scrollTo();
}
},

activateCurrent() {
if (this.app.layout.detailsShow()) {
this.app.workspaceDetailsView.$('#coding-rules-quality-profile-activate').click();
} else {
const rule = this.app.list.at(this.app.state.get('selectedIndex'));
const ruleView = this.app.workspaceListView.children.findByModel(rule);
ruleView.$('.coding-rules-detail-quality-profile-activate').click();
}
},

deactivateCurrent() {
if (!this.app.layout.detailsShow()) {
const rule = this.app.list.at(this.app.state.get('selectedIndex'));
const ruleView = this.app.workspaceListView.children.findByModel(rule);
ruleView.$('.coding-rules-detail-quality-profile-deactivate').click();
}
},

updateActivation(rule, actives) {
const selectedProfile = this.options.app.state.get('query').qprofile;
if (selectedProfile) {
const profile = (actives || []).find(activation => activation.qProfile === selectedProfile);
const listRule = this.app.list.get(rule.id);
if (profile && listRule) {
listRule.set('activation', {
...listRule.get('activation'),
inherit: profile.inherit,
severity: profile.severity
});
}
}
}
});

+ 0
- 57
server/sonar-web/src/main/js/apps/coding-rules/facets-view.js View File

@@ -1,57 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 QueryFacet from './facets/query-facet';
import KeyFacet from './facets/key-facet';
import LanguageFacet from './facets/language-facet';
import RepositoryFacet from './facets/repository-facet';
import TagFacet from './facets/tag-facet';
import QualityProfileFacet from './facets/quality-profile-facet';
import SeverityFacet from './facets/severity-facet';
import StatusFacet from './facets/status-facet';
import AvailableSinceFacet from './facets/available-since-facet';
import InheritanceFacet from './facets/inheritance-facet';
import ActiveSeverityFacet from './facets/active-severity-facet';
import TemplateFacet from './facets/template-facet';
import TypeFacet from './facets/type-facet';

const viewsMapping = {
q: QueryFacet,
rule_key: KeyFacet,
languages: LanguageFacet,
repositories: RepositoryFacet,
tags: TagFacet,
qprofile: QualityProfileFacet,
severities: SeverityFacet,
statuses: StatusFacet,
available_since: AvailableSinceFacet,
inheritance: InheritanceFacet,
active_severities: ActiveSeverityFacet,
is_template: TemplateFacet,
types: TypeFacet
};

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

+ 0
- 61
server/sonar-web/src/main/js/apps/coding-rules/facets/active-severity-facet.js View File

@@ -1,61 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-severity-facet.hbs';
import { translate } from '../../../helpers/l10n';

export default BaseFacet.extend({
template: Template,
severities: ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'],

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

onQueryChange() {
const query = this.options.app.state.get('query');
const isProfileSelected = query.qprofile != null;
const isActiveShown = '' + query.activation === 'true';
if (!isProfileSelected || !isActiveShown) {
this.forbid();
}
},

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

forbid() {
BaseFacet.prototype.forbid.apply(this, arguments);
this.$el.prop('title', translate('coding_rules.filters.active_severity.inactive'));
},

allow() {
BaseFacet.prototype.allow.apply(this, arguments);
this.$el.prop('title', null);
},

sortValues(values) {
const order = this.severities;
return sortBy(values, v => order.indexOf(v.val));
}
});

+ 0
- 57
server/sonar-web/src/main/js/apps/coding-rules/facets/available-since-facet.js View File

@@ -1,57 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-available-since-facet.hbs';

export default BaseFacet.extend({
template: Template,

events() {
return {
...BaseFacet.prototype.events.apply(this, arguments),
'change input': 'applyFacet'
};
},

onRender() {
this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled'));
this.$el.attr('data-property', this.model.get('property'));
this.$('input').datepicker({
dateFormat: 'yy-mm-dd',
changeMonth: true,
changeYear: true
});
const value = this.options.app.state.get('query').available_since;
if (value) {
this.$('input').val(value);
}
},

applyFacet() {
const obj = {};
const property = this.model.get('property');
obj[property] = this.$('input').val();
this.options.app.state.updateFilter(obj);
},

getLabelsSource() {
return this.options.app.languages;
}
});

+ 0
- 26
server/sonar-web/src/main/js/apps/coding-rules/facets/base-facet.js View File

@@ -1,26 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-base-facet.hbs';

export default BaseFacet.extend({
className: 'search-navigator-facet-box',
template: Template
});

+ 0
- 41
server/sonar-web/src/main/js/apps/coding-rules/facets/custom-labels-facet.js View File

@@ -1,41 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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({
getLabelsSource() {
return [];
},

getValues() {
const that = this;
const labels = that.getLabelsSource();
return this.model.getValues().map(item => {
return { ...item, label: labels[item.val] };
});
},

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

+ 0
- 87
server/sonar-web/src/main/js/apps/coding-rules/facets/custom-values-facet.js View File

@@ -1,87 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-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() {
return window.baseUrl;
},

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

prepareSearch() {
this.$('.js-custom-value').select2({
placeholder: translate('search_verb'),
minimumInputLength: 1,
allowClear: false,
formatNoMatches() {
return translate('select2.noMatches');
},
formatSearching() {
return translate('select2.searching');
},
formatInputTooShort() {
return translateWithParameters('select2.tooShort', 1);
},
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;
this.options.app.state.updateFilter(obj);
}
});

+ 0
- 87
server/sonar-web/src/main/js/apps/coding-rules/facets/inheritance-facet.js View File

@@ -1,87 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-inheritance-facet.hbs';
import { translate } from '../../../helpers/l10n';

export default BaseFacet.extend({
template: Template,

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

onQueryChange() {
const query = this.options.app.state.get('query');
const isProfileSelected = query.qprofile != null;
if (isProfileSelected) {
const profile = this.options.app.qualityProfiles.find(p => p.key === query.qprofile);
if (profile != null && profile.parentKey == null) {
this.forbid();
}
} else {
this.forbid();
}
},

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

forbid() {
BaseFacet.prototype.forbid.apply(this, arguments);
this.$el.prop('title', translate('coding_rules.filters.inheritance.inactive'));
},

allow() {
BaseFacet.prototype.allow.apply(this, arguments);
this.$el.prop('title', null);
},

getValues() {
const values = ['NONE', 'INHERITED', 'OVERRIDES'];
return values.map(key => {
return {
label: translate('coding_rules.filters.inheritance', key.toLowerCase()),
val: key
};
});
},

toggleFacet(e) {
const obj = {};
const property = this.model.get('property');
if ($(e.currentTarget).is('.active')) {
obj[property] = null;
} else {
obj[property] = $(e.currentTarget).data('value');
}
this.options.app.state.updateFilter(obj);
},

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

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

@@ -1,40 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-key-facet.hbs';

export default BaseFacet.extend({
template: Template,

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

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

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

+ 0
- 63
server/sonar-web/src/main/js/apps/coding-rules/facets/language-facet.js View File

@@ -1,63 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/languages/list';
},

prepareAjaxSearch() {
return {
quietMillis: 300,
url: this.getUrl(),
data(term) {
return { q: term, ps: 10000 };
},
results(data) {
return {
more: false,
results: data.languages.map(lang => {
return { id: lang.key, text: lang.name };
})
};
}
};
},

getLabelsSource() {
return this.options.app.languages;
},

getValues() {
const that = this;
const labels = that.getLabelsSource();
return this.model.getValues().map(item => {
return { ...item, label: labels[item.val] };
});
},

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

+ 0
- 132
server/sonar-web/src/main/js/apps/coding-rules/facets/quality-profile-facet.js View File

@@ -1,132 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-quality-profile-facet.hbs';

export default BaseFacet.extend({
template: Template,

events() {
return {
...BaseFacet.prototype.events.apply(this, arguments),
'click .js-active': 'setActivation',
'click .js-inactive': 'unsetActivation'
};
},

onRender() {
BaseFacet.prototype.onRender.apply(this, arguments);
const compareToProfile = this.options.app.state.get('query').compareToProfile;
if (typeof compareToProfile === 'string') {
const facet = this.$('.js-facet').filter(`[data-value="${compareToProfile}"]`);
if (facet.length > 0) {
facet.addClass('active compare');
}
}
},

getValues() {
const that = this;
const languagesQuery = this.options.app.state.get('query').languages;
const languages = languagesQuery != null ? languagesQuery.split(',') : [];
const lang = languages.length === 1 ? languages[0] : null;
const values = this.options.app.qualityProfiles
.filter(profile => (lang != null ? profile.language === lang : true))
.map(profile => ({
extra: that.options.app.languages[profile.language],
isBuiltIn: profile.isBuiltIn,
label: profile.name,
val: profile.key
}));
const compareProfile = this.options.app.state.get('query').compareToProfile;
if (compareProfile != null) {
const property = this.model.get('property');
const selectedProfile = this.options.app.state.get('query')[property];
return sortBy(values, [
profile => (profile.val === compareProfile || profile.val === selectedProfile ? 0 : 1),
'label'
]);
}
return sortBy(values, 'label');
},

toggleFacet(e) {
const obj = {};
const property = this.model.get('property');
if ($(e.currentTarget).is('.active')) {
obj.activation = null;
obj[property] = null;
} else {
obj.activation = true;
obj[property] = $(e.currentTarget).data('value');
}
obj.compareToProfile = null;
this.options.app.state.updateFilter(obj);
},

setActivation(e) {
e.stopPropagation();
const compareProfile = this.options.app.state.get('query').compareToProfile;
const profile = $(e.currentTarget)
.parents('.js-facet')
.data('value');
if (compareProfile == null || compareProfile !== profile) {
this.options.app.state.updateFilter({ activation: 'true', compareToProfile: null });
}
},

unsetActivation(e) {
e.stopPropagation();
const compareProfile = this.options.app.state.get('query').compareToProfile;
const profile = $(e.currentTarget)
.parents('.js-facet')
.data('value');
if (compareProfile == null || compareProfile !== profile) {
this.options.app.state.updateFilter({
activation: 'false',
active_severities: null,
compareToProfile: null
});
}
},

getToggled() {
const activation = this.options.app.state.get('query').activation;
return activation === 'true' || activation === true;
},

disable() {
const obj = { activation: null };
const property = this.model.get('property');
obj[property] = null;
obj.compareToProfile = null;
this.options.app.state.updateFilter(obj);
},

serializeData() {
return {
...BaseFacet.prototype.serializeData.apply(this, arguments),
values: this.getValues(),
toggled: this.getToggled()
};
}
});

+ 0
- 83
server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js View File

@@ -1,83 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { debounce } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/coding-rules-query-facet.hbs';

export default BaseFacet.extend({
template: Template,

events(...args) {
return {
...BaseFacet.prototype.events.apply(this, args),
'submit form': 'onFormSubmit',
'keyup input': 'onKeyUp',
'search input': 'onSearch',
'click .js-reset': 'onResetClick'
};
},

onRender() {
this.$el.attr('data-property', this.model.get('property'));
const query = this.options.app.state.get('query');
const value = query.q;
if (value != null) {
this.$('input').val(value);
this.$('.js-hint').toggleClass('hidden', value.length !== 1);
this.$('.js-reset').toggleClass('hidden', value.length === 0);
}
},

onFormSubmit(e) {
e.preventDefault();
this.applyFacet();
},

onKeyUp() {
const q = this.$('input').val();
this.$('.js-hint').toggleClass('hidden', q.length !== 1);
this.$('.js-reset').toggleClass('hidden', q.length === 0);
},

onSearch() {
const q = this.$('input').val();
if (q.length !== 1) {
this.applyFacet();
}
},

onResetClick(e) {
e.preventDefault();
this.$('input')
.val('')
.focus();
},

applyFacet() {
const obj = {};
const property = this.model.get('property');
const value = this.$('input').val();
if (this.buffer !== value) {
this.buffer = value;
obj[property] = value;
this.options.app.state.updateFilter(obj, { force: true });
}
}
});

+ 0
- 70
server/sonar-web/src/main/js/apps/coding-rules/facets/repository-facet.js View File

@@ -1,70 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/rules/repositories';
},

prepareAjaxSearch() {
return {
quietMillis: 300,
url: this.getUrl(),
data(term) {
return { q: term, ps: 10000 };
},
results(data) {
return {
more: false,
results: data.repositories.map(repo => {
return { id: repo.key, text: repo.name + ' (' + repo.language + ')' };
})
};
}
};
},

getLabelsSource() {
const source = {};
this.options.app.repositories.forEach(repo => (source[repo.key] = repo.name));
return source;
},

getValues() {
const that = this;
const labels = that.getLabelsSource();
return this.model.getValues().map(value => {
const repo = that.options.app.repositories.find(repo => repo.key === value.val);
if (repo != null) {
const langName = that.options.app.languages[repo.language];
Object.assign(value, { extra: langName });
}
return { ...value, label: labels[value.val] };
});
},

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

+ 0
- 32
server/sonar-web/src/main/js/apps/coding-rules/facets/severity-facet.js View File

@@ -1,32 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-severity-facet.hbs';

export default BaseFacet.extend({
template: Template,
severities: ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'],

sortValues(values) {
const order = this.severities;
return sortBy(values, v => order.indexOf(v.val));
}
});

+ 0
- 46
server/sonar-web/src/main/js/apps/coding-rules/facets/tag-facet.js View File

@@ -1,46 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/rules/tags';
},

prepareAjaxSearch() {
return {
quietMillis: 300,
url: this.getUrl(),
data: term => ({
organization: this.options.app.organization,
q: term,
ps: 100
}),
results(data) {
return {
more: false,
results: data.tags.map(tag => {
return { id: tag, text: tag };
})
};
}
};
}
});

+ 0
- 48
server/sonar-web/src/main/js/apps/coding-rules/facets/template-facet.js View File

@@ -1,48 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-template-facet.hbs';

export default BaseFacet.extend({
template: Template,

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

toggleFacet(e) {
$(e.currentTarget).toggleClass('active');
const property = this.model.get('property');
const obj = {};
if ($(e.currentTarget).hasClass('active')) {
obj[property] = '' + $(e.currentTarget).data('value');
} else {
obj[property] = null;
}
this.options.app.state.updateFilter(obj);
}
});

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

@@ -1,31 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-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
- 129
server/sonar-web/src/main/js/apps/coding-rules/init.js View File

@@ -1,129 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 $ 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';
import Facets from '../../components/navigator/models/facets';
import Controller from './controller';
import Router from '../../components/navigator/router';
import WorkspaceListView from './workspace-list-view';
import WorkspaceHeaderView from './workspace-header-view';
import FacetsView from './facets-view';
import { searchQualityProfiles } from '../../api/quality-profiles';
import { getRulesApp } from '../../api/rules';
import { areThereCustomOrganizations } from '../../store/organizations/utils';

const App = new Marionette.Application();

App.on('start', function(
options /*: {
el: HTMLElement,
organization: ?string,
isDefaultOrganization: boolean
} */
) {
App.organization = options.organization;
const data = options.organization ? { organization: options.organization } : {};
Promise.all([getRulesApp(data), searchQualityProfiles(data)])
.then(([appResponse, profilesResponse]) => {
App.customRules = !areThereCustomOrganizations();
App.canWrite = appResponse.canWrite;
App.organization = options.organization;
App.qualityProfiles = sortBy(profilesResponse.profiles, ['name', 'lang']);
App.languages = { ...appResponse.languages, none: 'None' };
App.repositories = appResponse.repositories;
App.statuses = appResponse.statuses;

this.layout = new Layout({ el: options.el });
this.layout.render();
$('#footer').addClass('page-footer-with-sidebar');

const allFacets = [
'q',
'rule_key',
'languages',
'types',
'tags',
'repositories',
'severities',
'statuses',
'available_since',
App.customRules ? 'is_template' : null,
'qprofile',
'inheritance',
'active_severities'
].filter(f => f != null);

this.state = new State({ allFacets });
this.list = new Rules();
this.facets = new Facets();

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

this.workspaceListView = new WorkspaceListView({
app: this,
collection: this.list
});
this.layout.workspaceListRegion.show(this.workspaceListView);
this.workspaceListView.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);

key.setScope('list');
this.router = new Router({
app: this
});
Backbone.history.start();
})
.catch(() => {
// do nothing in case of WS error
});
});

export default function(
el /*: HTMLElement */,
organization /*: ?string */,
isDefaultOrganization /*: boolean */
) {
App.start({ el, organization, isDefaultOrganization });

return () => {
// $FlowFixMe
Backbone.history.stop();
App.layout.destroy();
$('#footer').removeClass('page-footer-with-sidebar');
};
}

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

@@ -1,55 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/coding-rules-layout.hbs';

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

regions: {
facetsRegion: '.layout-page-filters',
workspaceHeaderRegion: '.coding-rules-header',
workspaceListRegion: '.coding-rules-list',
workspaceDetailsRegion: '.coding-rules-details'
},

onRender() {
const navigator = this.$('.layout-page');
const top = navigator.offset().top;
this.$('.layout-page-side').css({ top });
},

showDetails() {
this.scroll = $(window).scrollTop();
this.$('.coding-rules').addClass('coding-rules-extended-view');
},

hideDetails() {
this.$('.coding-rules').removeClass('coding-rules-extended-view');
if (this.scroll != null) {
$(window).scrollTop(this.scroll);
}
},

detailsShow() {
return this.$('.coding-rules').is('.coding-rules-extended-view');
}
});

+ 0
- 49
server/sonar-web/src/main/js/apps/coding-rules/models/rule.js View File

@@ -1,49 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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';

export default Backbone.Model.extend({
idAttribute: 'key',

addExtraAttributes(repositories) {
const repo = repositories.find(repo => repo.key === this.get('repo')) || this.get('repo');
const repoName = repo != null ? repo.name : repo;
const isManual = this.get('repo') === 'manual';
const isCustom = this.has('templateKey');
this.set(
{
repoName,
isManual,
isCustom
},
{ silent: true }
);
},

getInactiveProfiles(actives, profiles) {
return actives.map(profile => {
const profileBase = profiles.find(p => p.key === profile.qProfile);
if (profileBase != null) {
Object.assign(profile, profileBase);
}
return profile;
});
}
});

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

@@ -1,59 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 Rule from './rule';

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

parseRules(r) {
let rules = r.rules;
const profiles = r.qProfiles || [];

if (r.actives != null) {
rules = rules.map(rule => {
const activations = (r.actives[rule.key] || []).map(activation => {
const profile = profiles[activation.qProfile];
if (profile != null) {
Object.assign(activation, { profile });
if (profile.parent != null) {
Object.assign(activation, { parentProfile: profiles[profile.parent] });
}
}
return activation;
});
return { ...rule, activation: activations.length > 0 ? activations[0] : null };
});
}
return rules;
},

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

addExtraAttributes(repositories) {
this.models.forEach(model => {
model.addExtraAttributes(repositories);
});
}
});

+ 0
- 39
server/sonar-web/src/main/js/apps/coding-rules/models/state.js View File

@@ -1,39 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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: ['types', 'languages'],
facetsFromServer: [
'languages',
'repositories',
'tags',
'severities',
'statuses',
'active_severities',
'types'
],
transform: {}
}
});

+ 160
- 0
server/sonar-web/src/main/js/apps/coding-rules/query.ts View File

@@ -0,0 +1,160 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { RuleInheritance } from '../../app/types';
import {
RawQuery,
parseAsString,
parseAsArray,
serializeString,
serializeStringArray,
cleanQuery,
queriesEqual,
parseAsDate,
serializeDateShort,
parseAsOptionalBoolean,
serializeOptionalBoolean,
parseAsOptionalString
} from '../../helpers/query';

export interface Query {
activation: boolean | undefined;
activationSeverities: string[];
availableSince: Date | undefined;
compareToProfile: string | undefined;
inheritance: RuleInheritance | undefined;
languages: string[];
profile: string | undefined;
repositories: string[];
ruleKey: string | undefined;
searchQuery: string | undefined;
severities: string[];
statuses: string[];
tags: string[];
template: boolean | undefined;
types: string[];
}

export type FacetKey = keyof Query;

export interface Facet {
[value: string]: number;
}

export type Facets = { [F in FacetKey]?: Facet };

export type OpenFacets = { [F in FacetKey]?: boolean };

export interface Activation {
inherit: string;
severity: string;
}

export interface Actives {
[rule: string]: {
[profile: string]: Activation;
};
}

export function parseQuery(query: RawQuery): Query {
return {
activation: parseAsOptionalBoolean(query.activation),
activationSeverities: parseAsArray(query.active_severities, parseAsString),
availableSince: parseAsDate(query.available_since),
compareToProfile: parseAsOptionalString(query.compareToProfile),
inheritance: parseAsInheritance(query.inheritance),
languages: parseAsArray(query.languages, parseAsString),
profile: parseAsOptionalString(query.qprofile),
repositories: parseAsArray(query.repositories, parseAsString),
ruleKey: parseAsOptionalString(query.rule_key),
searchQuery: parseAsOptionalString(query.q),
severities: parseAsArray(query.severities, parseAsString),
statuses: parseAsArray(query.statuses, parseAsString),
tags: parseAsArray(query.tags, parseAsString),
template: parseAsOptionalBoolean(query.is_template),
types: parseAsArray(query.types, parseAsString)
};
}

export function serializeQuery(query: Query): RawQuery {
/* eslint-disable camelcase */
return cleanQuery({
activation: serializeOptionalBoolean(query.activation),
active_severities: serializeStringArray(query.activationSeverities),
available_since: serializeDateShort(query.availableSince),
compareToProfile: serializeString(query.compareToProfile),
inheritance: serializeInheritance(query.inheritance),
is_template: serializeOptionalBoolean(query.template),
languages: serializeStringArray(query.languages),
q: serializeString(query.searchQuery),
qprofile: serializeString(query.profile),
repositories: serializeStringArray(query.repositories),
rule_key: serializeString(query.ruleKey),
severities: serializeStringArray(query.severities),
statuses: serializeStringArray(query.statuses),
tags: serializeStringArray(query.tags),
types: serializeStringArray(query.types)
});
/* eslint-enable camelcase */
}

export function areQueriesEqual(a: RawQuery, b: RawQuery) {
return queriesEqual(parseQuery(a), parseQuery(b));
}

export function shouldRequestFacet(facet: FacetKey) {
const facetsToRequest = [
'activationSeverities',
'languages',
'repositories',
'severities',
'statuses',
'tags',
'types'
];
return facetsToRequest.includes(facet);
}

export function getServerFacet(facet: FacetKey) {
return facet === 'activationSeverities' ? 'active_severities' : facet;
}

export function getAppFacet(serverFacet: string): FacetKey {
return serverFacet === 'active_severities' ? 'activationSeverities' : (serverFacet as FacetKey);
}

export function getOpen(query: RawQuery) {
return query.open;
}

function parseAsInheritance(value?: string): RuleInheritance | undefined {
if (value === RuleInheritance.Inherited) {
return RuleInheritance.Inherited;
} else if (value === RuleInheritance.NotInherited) {
return RuleInheritance.NotInherited;
} else if (value === RuleInheritance.Overridden) {
return RuleInheritance.Overridden;
} else {
return undefined;
}
}

function serializeInheritance(value: RuleInheritance | undefined): string | undefined {
return value;
}

+ 27
- 2
server/sonar-web/src/main/js/apps/coding-rules/routes.ts View File

@@ -17,13 +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 { RouterState, RouteComponent } from 'react-router';
import { RouterState, RouteComponent, RedirectFunction } from 'react-router';
import { parseQuery, serializeQuery } from './query';
import { RawQuery } from '../../helpers/query';

function parseHash(hash: string): RawQuery {
const query: RawQuery = {};
const parts = hash.split('|');
parts.forEach(part => {
const tokens = part.split('=');
if (tokens.length === 2) {
query[decodeURIComponent(tokens[0])] = decodeURIComponent(tokens[1]);
}
});
return query;
}

const routes = [
{
indexRoute: {
onEnter: (nextState: RouterState, replace: RedirectFunction) => {
const { hash } = window.location;
if (hash.length > 1) {
const query = parseHash(hash.substr(1));
const normalizedQuery = {
...serializeQuery(parseQuery(query)),
open: query.open
};
replace({ pathname: nextState.location.pathname, query: normalizedQuery });
}
},
getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
import('./components/CodingRulesAppContainer').then(i => callback(null, i.default));
import('./components/App').then(i => callback(null, i.default));
}
}
}

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

@@ -1,185 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { 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';
import ParamView from './rule/rule-parameters-view';
import ProfilesView from './rule/rule-profiles-view';
import CustomRulesView from './rule/custom-rules-view';
import CustomRuleCreationView from './rule/custom-rule-creation-view';
import DeleteRuleView from './rule/delete-rule-view';
import IssuesView from './rule/rule-issues-view';
import Template from './templates/coding-rules-rule-details.hbs';
import { searchRules } from '../../api/rules';

export default Marionette.LayoutView.extend({
className: 'coding-rule-details',
template: Template,

regions: {
metaRegion: '.js-rule-meta',
descRegion: '.js-rule-description',
paramRegion: '.js-rule-parameters',
profilesRegion: '.js-rule-profiles',
customRulesRegion: '.js-rule-custom-rules',
issuesRegion: '.js-rule-issues'
},

events: {
'click .js-edit-custom': 'editCustomRule',
'click .js-delete': 'deleteRule'
},

initialize() {
this.bindShortcuts();
this.customRules = new Rules();
if (this.model.get('isTemplate')) {
this.fetchCustomRules();
}
this.listenTo(this.options.app.state, 'change:selectedIndex', this.select);
},

onRender() {
this.metaRegion.show(
new MetaView({
app: this.options.app,
model: this.model
})
);
this.descRegion.show(
new DescView({
app: this.options.app,
model: this.model
})
);
this.paramRegion.show(
new ParamView({
app: this.options.app,
model: this.model
})
);
this.profilesRegion.show(
new ProfilesView({
app: this.options.app,
model: this.model,
collection: new Backbone.Collection(this.getQualityProfiles())
})
);
this.customRulesRegion.show(
new CustomRulesView({
app: this.options.app,
model: this.model,
collection: this.customRules
})
);
this.issuesRegion.show(
new IssuesView({
app: this.options.app,
model: this.model
})
);
this.$el.scrollParent().scrollTop(0);
},

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

fetchCustomRules() {
const options = {
template_key: this.model.get('key'),
f: 'name,severity,params'
};
searchRules(options).then(r => this.customRules.reset(r.rules), () => {});
},

getQualityProfiles() {
return this.model.getInactiveProfiles(this.options.actives, this.options.app.qualityProfiles);
},

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

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

editCustomRule() {
new CustomRuleCreationView({
app: this.options.app,
model: this.model
}).render();
},

deleteRule() {
const deleteRuleView = new DeleteRuleView({
model: this.model
}).render();

deleteRuleView.on('delete', () => {
const { controller } = this.options.app;
if (controller.isRulePermalink()) {
controller.newSearch();
} else {
controller.fetchList();
}
});
},

select() {
const selected = this.options.app.state.get('selectedIndex');
const selectedRule = this.options.app.list.at(selected);
this.options.app.controller.showDetails(selectedRule);
},

serializeData() {
const isCustom = this.model.has('templateKey');
const isEditable = this.options.app.canWrite && this.options.app.customRules && isCustom;
let qualityProfilesVisible = true;

if (this.model.get('isTemplate')) {
qualityProfilesVisible = Object.keys(this.options.actives).length > 0;
}

return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
isEditable,
qualityProfilesVisible,
allTags: union(this.model.get('sysTags'), this.model.get('tags'))
};
}
});

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

@@ -1,41 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { union } from 'lodash';
import ActionOptionsView from '../../components/common/action-options-view';
import Template from './templates/coding-rules-rule-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);
return ActionOptionsView.prototype.selectOption.apply(this, arguments);
},

serializeData() {
return {
...ActionOptionsView.prototype.serializeData.apply(this, arguments),
tags: union(this.model.get('sysTags'), this.model.get('tags'))
};
}
});

+ 0
- 224
server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js View File

@@ -1,224 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 ModalFormView from '../../../components/common/modal-form';
import Template from '../templates/rule/coding-rules-custom-rule-creation.hbs';
import { csvEscape } from '../../../helpers/csv';
import latinize from '../../../helpers/latinize';
import { translate } from '../../../helpers/l10n';

export default ModalFormView.extend({
template: Template,

ui() {
return {
...ModalFormView.prototype.ui.apply(this, arguments),
customRuleCreationKey: '#coding-rules-custom-rule-creation-key',
customRuleCreationName: '#coding-rules-custom-rule-creation-name',
customRuleCreationHtmlDescription: '#coding-rules-custom-rule-creation-html-description',
customRuleCreationType: '#coding-rules-custom-rule-creation-type',
customRuleCreationSeverity: '#coding-rules-custom-rule-creation-severity',
customRuleCreationStatus: '#coding-rules-custom-rule-creation-status',
customRuleCreationParameters: '[name]',
customRuleCreationCreate: '#coding-rules-custom-rule-creation-create',
customRuleCreationReactivate: '#coding-rules-custom-rule-creation-reactivate',
modalFoot: '.modal-foot'
};
},

events() {
return {
...ModalFormView.prototype.events.apply(this, arguments),
'input @ui.customRuleCreationName': 'generateKey',
'keydown @ui.customRuleCreationName': 'generateKey',
'keyup @ui.customRuleCreationName': 'generateKey',

'input @ui.customRuleCreationKey': 'flagKey',
'keydown @ui.customRuleCreationKey': 'flagKey',
'keyup @ui.customRuleCreationKey': 'flagKey',

'click #coding-rules-custom-rule-creation-cancel': 'destroy',
'click @ui.customRuleCreationCreate': 'create',
'click @ui.customRuleCreationReactivate': 'reactivate'
};
},

generateKey() {
if (!this.keyModifiedByUser && this.ui.customRuleCreationKey) {
const generatedKey = latinize(this.ui.customRuleCreationName.val()).replace(
/[^A-Za-z0-9]/g,
'_'
);
this.ui.customRuleCreationKey.val(generatedKey);
}
},

flagKey() {
this.keyModifiedByUser = true;
},

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

this.keyModifiedByUser = false;

const format = function(state) {
if (!state.id) {
return state.text;
} else {
return `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${state.text}`;
}
};
const type = (this.model && this.model.get('type')) || this.options.templateRule.get('type');
const severity =
(this.model && this.model.get('severity')) || this.options.templateRule.get('severity');
const status =
(this.model && this.model.get('status')) || this.options.templateRule.get('status');

this.ui.customRuleCreationType.val(type);
this.ui.customRuleCreationType.select2({
width: '250px',
minimumResultsForSearch: 999
});

this.ui.customRuleCreationSeverity.val(severity);
this.ui.customRuleCreationSeverity.select2({
width: '250px',
minimumResultsForSearch: 999,
formatResult: format,
formatSelection: format
});

this.ui.customRuleCreationStatus.val(status);
this.ui.customRuleCreationStatus.select2({
width: '250px',
minimumResultsForSearch: 999
});
},

create(e) {
e.preventDefault();
const action = this.model && this.model.has('key') ? 'update' : 'create';
const options = {
name: this.ui.customRuleCreationName.val(),
markdown_description: this.ui.customRuleCreationHtmlDescription.val(),
type: this.ui.customRuleCreationType.val(),
severity: this.ui.customRuleCreationSeverity.val(),
status: this.ui.customRuleCreationStatus.val()
};
if (this.model && this.model.has('key')) {
options.key = this.model.get('key');
} else {
Object.assign(options, {
template_key: this.options.templateRule.get('key'),
custom_key: this.ui.customRuleCreationKey.val(),
prevent_reactivation: true
});
}
const params = this.ui.customRuleCreationParameters
.map(function() {
const node = $(this);
let value = node.val();
if (!value && action === 'create') {
value = node.prop('placeholder') || '';
}
return {
key: node.prop('name'),
value
};
})
.get();
options.params = params.map(param => param.key + '=' + csvEscape(param.value)).join(';');
this.sendRequest(action, options);
},

reactivate() {
const options = {
name: this.existingRule.name,
markdown_description: this.existingRule.mdDesc,
severity: this.existingRule.severity,
status: this.existingRule.status,
template_key: this.existingRule.templateKey,
custom_key: this.ui.customRuleCreationKey.val(),
prevent_reactivation: false
};
const params = this.existingRule.params;
options.params = params.map(param => param.key + '=' + param.defaultValue).join(';');
this.sendRequest('create', options);
},

sendRequest(action, options) {
this.$('.alert').addClass('hidden');
const that = this;
const url = window.baseUrl + '/api/rules/' + action;
return $.ajax({
url,
type: 'POST',
data: options,
statusCode: {
// do not show global error
400: null
}
})
.done(() => {
if (that.options.templateRule) {
that.options.app.controller.showDetails(that.options.templateRule);
} else {
that.options.app.controller.showDetails(that.model);
}
that.destroy();
})
.fail(jqXHR => {
if (jqXHR.status === 409) {
that.existingRule = jqXHR.responseJSON.rule;
that.showErrors([], [{ msg: translate('coding_rules.reactivate.help') }]);
that.ui.customRuleCreationCreate.addClass('hidden');
that.ui.customRuleCreationReactivate.removeClass('hidden');
} else {
that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
}
});
},

serializeData() {
let params = {};
if (this.options.templateRule) {
params = this.options.templateRule.get('params');
} else if (this.model && this.model.has('params')) {
params = this.model.get('params').map(p => ({ ...p, value: p.defaultValue }));
}

const statuses = ['READY', 'BETA', 'DEPRECATED'].map(status => {
return {
id: status,
text: translate('rules.status', status.toLowerCase())
};
});

return {
...ModalFormView.prototype.serializeData.apply(this, arguments),
params,
statuses,
change: this.model && this.model.has('key'),
severities: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'],
types: ['BUG', 'VULNERABILITY', 'CODE_SMELL']
};
}
});

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

@@ -1,55 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 DeleteRuleView from './delete-rule-view';
import Template from '../templates/rule/coding-rules-custom-rule.hbs';

export default Marionette.ItemView.extend({
tagName: 'tr',
template: Template,

modelEvents: {
change: 'render'
},

events: {
'click .js-delete-custom-rule': 'deleteRule'
},

deleteRule() {
const deleteRuleView = new DeleteRuleView({
model: this.model
}).render();

deleteRuleView.on('delete', () => {
this.model.collection.remove(this.model);
this.destroy();
});
},

serializeData() {
return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
canDeleteCustomRule: this.options.app.customRules && this.options.app.canWrite,
templateRule: this.options.templateRule,
permalink: window.baseUrl + '/coding_rules/#rule_key=' + encodeURIComponent(this.model.id)
};
}
});

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

@@ -1,62 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 CustomRuleView from './custom-rule-view';
import CustomRuleCreationView from './custom-rule-creation-view';
import Template from '../templates/rule/coding-rules-custom-rules.hbs';

export default Marionette.CompositeView.extend({
template: Template,
childView: CustomRuleView,
childViewContainer: '#coding-rules-detail-custom-rules',

childViewOptions() {
return {
app: this.options.app,
templateRule: this.model
};
},

modelEvents: {
change: 'render'
},

events: {
'click .js-create-custom-rule': 'createCustomRule'
},

onRender() {
this.$el.toggleClass('hidden', !this.model.get('isTemplate'));
},

createCustomRule() {
new CustomRuleCreationView({
app: this.options.app,
templateRule: this.model
}).render();
},

serializeData() {
return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
canCreateCustomRule: this.options.app.customRules && this.options.app.canWrite
};
}
});

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

@@ -1,37 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 ModalFormView from '../../../components/common/modal-form';
import Template from '../templates/rule/coding-rules-delete-rule.hbs';
import { deleteRule } from '../../../api/rules';

export default ModalFormView.extend({
template: Template,

onFormSubmit() {
ModalFormView.prototype.onFormSubmit.apply(this, arguments);
deleteRule({ key: this.model.id }).then(
() => {
this.destroy();
this.trigger('delete');
},
() => {}
);
}
});

+ 0
- 178
server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js View File

@@ -1,178 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 ModalForm from '../../../components/common/modal-form';
import Template from '../templates/rule/coding-rules-profile-activation.hbs';
import { csvEscape } from '../../../helpers/csv';
import { sortProfiles } from '../../quality-profiles/utils';

export default ModalForm.extend({
template: Template,

ui() {
return {
...ModalForm.prototype.ui.apply(this, arguments),
qualityProfileSelect: '#coding-rules-quality-profile-activation-select',
qualityProfileSeverity: '#coding-rules-quality-profile-activation-severity',
qualityProfileActivate: '#coding-rules-quality-profile-activation-activate',
qualityProfileParameters: '[name]'
};
},

events() {
return {
...ModalForm.prototype.events.apply(this, arguments),
'click @ui.qualityProfileActivate': 'activate'
};
},

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

this.ui.qualityProfileSelect.select2({
width: '250px',
minimumResultsForSearch: 5
});

const that = this;
const format = function(state) {
if (!state.id) {
return state.text;
} else {
return `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${state.text}`;
}
};
const severity =
(this.model && this.model.get('severity')) || this.options.rule.get('severity');
this.ui.qualityProfileSeverity.val(severity);
this.ui.qualityProfileSeverity.select2({
width: '250px',
minimumResultsForSearch: 999,
formatResult: format,
formatSelection: format
});
setTimeout(() => {
that
.$('a')
.first()
.focus();
}, 0);
},

activate(e) {
e.preventDefault();
const that = this;
let profileKey = this.ui.qualityProfileSelect.val();
const params = this.ui.qualityProfileParameters
.map(function() {
return {
key: $(this).prop('name'),
value: $(this).val() || $(this).prop('placeholder') || ''
};
})
.get();
const paramsHash = params.map(param => param.key + '=' + csvEscape(param.value)).join(';');

if (this.model) {
profileKey = this.model.get('qProfile');
if (!profileKey) {
profileKey = this.model.get('key');
}
}

const severity = this.ui.qualityProfileSeverity.val();
const ruleKey = this.options.rule.get('key');

this.disableForm();

return $.ajax({
type: 'POST',
url: window.baseUrl + '/api/qualityprofiles/activate_rule',
data: {
severity,
profile_key: profileKey,
rule_key: ruleKey,
params: paramsHash
},
statusCode: {
// do not show global error
400: null
}
})
.done(() => {
that.destroy();
that.trigger('profileActivated', severity, params, profileKey);
})
.fail(jqXHR => {
that.enableForm();
that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
});
},

getAvailableQualityProfiles(lang) {
const activeQualityProfiles = this.collection || new Backbone.Collection();
const inactiveProfiles = this.options.app.qualityProfiles.filter(
profile => !activeQualityProfiles.findWhere({ key: profile.key })
);
// choose QP which a user can administrate, which are the same language and which are not built-in
return inactiveProfiles
.filter(profile => profile.actions && profile.actions.edit)
.filter(profile => profile.language === lang)
.filter(profile => !profile.isBuiltIn);
},

serializeData() {
let params = this.options.rule.get('params');
if (this.model != null) {
const modelParams = this.model.get('params');
if (Array.isArray(modelParams)) {
params = params.map(p => {
const parentParam = modelParams.find(param => param.key === p.key);
if (parentParam != null) {
Object.assign(p, { value: parentParam.value });
}
return p;
});
}
}

const availableProfiles = this.getAvailableQualityProfiles(this.options.rule.get('lang'));
const contextProfile = this.options.app.state.get('query').qprofile;

// decrease depth by 1, so the top level starts at 0
const profilesWithDepth = sortProfiles(availableProfiles).map(profile => ({
...profile,
depth: profile.depth - 1
}));

return {
...ModalForm.prototype.serializeData.apply(this, arguments),
params,
contextProfile,
change: this.model && this.model.has('severity'),
qualityProfiles: profilesWithDepth,
severities: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'],
saveEnabled: availableProfiles.length > 0 || (this.model && this.model.get('qProfile')),
isCustomRule:
(this.model && this.model.has('templateKey')) || this.options.rule.has('templateKey')
};
}
});

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

@@ -1,107 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/rule/coding-rules-rule-description.hbs';
import confirmDialog from '../confirm-dialog';
import { translate } from '../../../helpers/l10n';

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

modelEvents: {
change: 'render'
},

ui: {
descriptionExtra: '#coding-rules-detail-description-extra',
extendDescriptionLink: '#coding-rules-detail-extend-description',
extendDescriptionForm: '.coding-rules-detail-extend-description-form',
extendDescriptionSubmit: '#coding-rules-detail-extend-description-submit',
extendDescriptionRemove: '#coding-rules-detail-extend-description-remove',
extendDescriptionText: '#coding-rules-detail-extend-description-text',
cancelExtendDescription: '#coding-rules-detail-extend-description-cancel'
},

events: {
'click @ui.extendDescriptionLink': 'showExtendDescriptionForm',
'click @ui.cancelExtendDescription': 'hideExtendDescriptionForm',
'click @ui.extendDescriptionSubmit': 'submitExtendDescription',
'click @ui.extendDescriptionRemove': 'removeExtendedDescription'
},

showExtendDescriptionForm() {
this.ui.descriptionExtra.addClass('hidden');
this.ui.extendDescriptionForm.removeClass('hidden');
this.ui.extendDescriptionText.focus();
},

hideExtendDescriptionForm() {
this.ui.descriptionExtra.removeClass('hidden');
this.ui.extendDescriptionForm.addClass('hidden');
},

submitExtendDescription() {
const that = this;
this.ui.extendDescriptionForm.addClass('hidden');
const data = {
key: this.model.get('key'),
markdown_note: this.ui.extendDescriptionText.val()
};
if (this.options.app.organization) {
data.organization = this.options.app.organization;
}
return $.ajax({
type: 'POST',
url: window.baseUrl + '/api/rules/update',
dataType: 'json',
data
})
.done(r => {
that.model.set({
htmlNote: r.rule.htmlNote,
mdNote: r.rule.mdNote
});
that.render();
})
.fail(() => {
that.render();
});
},

removeExtendedDescription() {
const that = this;
confirmDialog({
html: translate('coding_rules.remove_extended_description.confirm'),
yesHandler() {
that.ui.extendDescriptionText.val('');
that.submitExtendDescription();
}
});
},

serializeData() {
return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
isCustom: this.model.get('isCustom'),
canCustomizeRule: this.options.app.canWrite
};
}
});

+ 0
- 42
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js View File

@@ -1,42 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 RuleFilterView from '../rule-filter-view';

export default {
onRuleFilterClick(e) {
e.preventDefault();
e.stopPropagation();
$('body').click();
const that = this;
const popup = new RuleFilterView({
triggerEl: $(e.currentTarget),
bottomRight: true,
model: this.model
});
popup.on('select', (property, value) => {
const obj = {};
obj[property] = '' + value;
that.options.app.state.updateFilter(obj);
popup.destroy();
});
popup.render();
}
};

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

@@ -1,97 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/rule/coding-rules-rule-issues.hbs';
import { searchIssues } from '../../../api/issues';
import { getPathUrlAsString, getComponentIssuesUrl, getBaseUrl } from '../../../helpers/urls';

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

initialize() {
this.total = null;
this.projects = [];
this.loading = true;
this.mounted = true;
this.requestIssues().then(
() => {
if (this.mounted) {
this.loading = false;
this.render();
}
},
() => {
this.loading = false;
}
);
},

onDestroy() {
this.mounted = false;
},

requestIssues() {
const parameters = {
rules: this.model.id,
resolved: false,
ps: 1,
facets: 'projectUuids'
};
const { organization } = this.options.app;
if (organization) {
Object.assign(parameters, { organization });
}
return searchIssues(parameters).then(r => {
const projectsFacet = r.facets.find(facet => facet.property === 'projectUuids');
let projects = projectsFacet != null ? projectsFacet.values : [];
projects = projects.map(project => {
const projectBase = r.components.find(component => component.uuid === project.val);
return {
...project,
name: projectBase != null ? projectBase.longName : '',
issuesUrl:
projectBase != null &&
getPathUrlAsString(
getComponentIssuesUrl(projectBase.key, {
resolved: 'false',
rules: this.model.id
})
)
};
});
this.projects = projects;
this.total = r.total;
});
},

serializeData() {
const { organization } = this.options.app;
const pathname = organization ? `/organizations/${organization}/issues` : '/issues';
const query = `?resolved=false&rules=${encodeURIComponent(this.model.id)}`;
const totalIssuesUrl = getBaseUrl() + pathname + query;
return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
loading: this.loading,
total: this.total,
totalIssuesUrl,
projects: this.projects
};
}
});

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

@@ -1,119 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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, union } from 'lodash';
import Marionette from 'backbone.marionette';
import RuleFilterMixin from './rule-filter-mixin';
import Template from '../templates/rule/coding-rules-rule-meta.hbs';
import { getRuleTags } from '../../../api/rules';

export default Marionette.ItemView.extend(RuleFilterMixin).extend({
template: Template,

modelEvents: {
change: 'render'
},

ui: {
tagsChange: '.coding-rules-detail-tags-change',
tagInput: '.coding-rules-detail-tag-input',
tagsEdit: '.coding-rules-detail-tag-edit',
tagsEditDone: '.coding-rules-detail-tag-edit-done',
tagsEditCancel: '.coding-rules-details-tag-edit-cancel',
tagsList: '.coding-rules-detail-tag-list'
},

events: {
'click @ui.tagsChange': 'changeTags',
'click @ui.tagsEditDone': 'editDone',
'click @ui.tagsEditCancel': 'cancelEdit',
'click .js-rule-filter': 'onRuleFilterClick'
},

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

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

changeTags() {
getRuleTags({ organization: this.options.app.organization }).then(
tags => {
this.ui.tagInput.select2({
tags: difference(difference(tags, this.model.get('tags')), this.model.get('sysTags')),
width: '300px'
});

this.ui.tagsEdit.removeClass('hidden');
this.ui.tagsList.addClass('hidden');
this.tagsBuffer = this.ui.tagInput.select2('val');
this.ui.tagInput.select2('open');
},
() => {}
);
},

cancelEdit() {
this.ui.tagsList.removeClass('hidden');
this.ui.tagsEdit.addClass('hidden');
if (this.ui.tagInput.select2) {
this.ui.tagInput.select2('val', this.tagsBuffer);
this.ui.tagInput.select2('close');
}
},

editDone() {
const that = this;
const tags = this.ui.tagInput.val();
const data = { key: this.model.get('key'), tags };
if (this.options.app.organization) {
data.organization = this.options.app.organization;
}
return $.ajax({
type: 'POST',
url: window.baseUrl + '/api/rules/update',
data
})
.done(r => {
that.model.set('tags', r.rule.tags);
that.cancelEdit();
})
.always(() => {
that.cancelEdit();
});
},

serializeData() {
const permalinkPath = this.options.app.organization
? `/organizations/${this.options.app.organization}/rules`
: '/coding_rules';

return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
canCustomizeRule: this.options.app.canWrite,
allTags: union(this.model.get('sysTags'), this.model.get('tags')),
permalink: window.baseUrl + permalinkPath + '#rule_key=' + encodeURIComponent(this.model.id)
};
}
});

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

@@ -1,34 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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/rule/coding-rules-rule-parameters.hbs';

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

modelEvents: {
change: 'render'
},

onRender() {
const params = this.model.get('params');
this.$el.toggleClass('hidden', params == null || params.length === 0);
}
});

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

@@ -1,180 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { stringify } from 'querystring';
import $ from 'jquery';
import { sortBy } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
import ProfileActivationView from './profile-activation-view';
import Template from '../templates/rule/coding-rules-rule-profile.hbs';
import confirmDialog from '../confirm-dialog';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export default Marionette.ItemView.extend({
tagName: 'tr',
template: Template,

modelEvents: {
change: 'render'
},

ui: {
change: '.coding-rules-detail-quality-profile-change',
revert: '.coding-rules-detail-quality-profile-revert',
deactivate: '.coding-rules-detail-quality-profile-deactivate'
},

events: {
'click @ui.change': 'change',
'click @ui.revert': 'revert',
'click @ui.deactivate': 'deactivate'
},

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

change() {
const that = this;
const activationView = new ProfileActivationView({
model: this.model,
collection: this.model.collection,
rule: this.options.rule,
app: this.options.app
});
activationView.on('profileActivated', () => {
that.options.refreshActives();
});
activationView.render();
},

revert() {
const that = this;
const ruleKey = this.options.rule.get('key');
confirmDialog({
title: translate('coding_rules.revert_to_parent_definition'),
html: translateWithParameters(
'coding_rules.revert_to_parent_definition.confirm',
this.getParent().name
),
yesLabel: translate('yes'),
noLabel: translate('cancel'),
yesHandler() {
return $.ajax({
type: 'POST',
url: window.baseUrl + '/api/qualityprofiles/activate_rule',
data: {
profile_key: that.model.get('qProfile'),
rule_key: ruleKey,
reset: true
}
}).done(() => {
that.options.refreshActives();
});
}
});
},

deactivate() {
const that = this;
const ruleKey = this.options.rule.get('key');
confirmDialog({
title: translate('coding_rules.deactivate'),
html: translateWithParameters('coding_rules.deactivate.confirm'),
yesLabel: translate('yes'),
noLabel: translate('cancel'),
yesHandler() {
return $.ajax({
type: 'POST',
url: window.baseUrl + '/api/qualityprofiles/deactivate_rule',
data: {
profile_key: that.model.get('qProfile'),
rule_key: ruleKey
}
}).done(() => {
that.options.refreshActives();
});
}
});
},

enableUpdate() {
return this.ui.update.prop('disabled', false);
},

getParent() {
if (!(this.model.get('inherit') && this.model.get('inherit') !== 'NONE')) {
return null;
}
const myProfile = this.options.app.qualityProfiles.find(
p => p.key === this.model.get('qProfile')
);
if (!myProfile) {
return null;
}
const parentKey = myProfile.parentKey;
const parent = { ...this.options.app.qualityProfiles.find(p => p.key === parentKey) };
const parentActiveInfo =
this.model.collection.findWhere({ qProfile: parentKey }) || new Backbone.Model();
Object.assign(parent, parentActiveInfo.toJSON());
return parent;
},

enhanceParameters(parent) {
const params = sortBy(this.model.get('params'), 'key');
if (!parent) {
return params;
}
return params.map(p => {
const parentParam = parent.params.find(param => param.key === p.key);
if (parentParam != null) {
return { ...p, original: parentParam.value };
} else {
return p;
}
});
},

getProfilePath(language, name) {
const { organization } = this.options.app;
const query = stringify({ language, name });
return organization
? `${window.baseUrl}/organizations/${organization}/quality_profiles/show?${query}`
: `${window.baseUrl}/profiles/show?${query}`;
},

serializeData() {
const parent = this.getParent();

return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
parent,
actions: this.model.get('actions') || {},
canWrite: this.options.app.canWrite,
parameters: this.enhanceParameters(parent),
templateKey: this.options.rule.get('templateKey'),
isTemplate: this.options.rule.get('isTemplate'),
profilePath: this.getProfilePath(this.model.get('language'), this.model.get('name')),
parentProfilePath: parent && this.getProfilePath(parent.language, parent.name)
};
}
});

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

@@ -1,101 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 ProfileView from './rule-profile-view';
import ProfileActivationView from './profile-activation-view';
import Template from '../templates/rule/coding-rules-rule-profiles.hbs';

export default Marionette.CompositeView.extend({
template: Template,
childView: ProfileView,
childViewContainer: '#coding-rules-detail-quality-profiles',

childViewOptions() {
return {
app: this.options.app,
rule: this.model,
refreshActives: this.refreshActives.bind(this)
};
},

modelEvents: {
change: 'render'
},

events: {
'click #coding-rules-quality-profile-activate': 'activate'
},

onRender() {
let qualityProfilesVisible = true;

if (this.model.get('isTemplate')) {
qualityProfilesVisible = this.collection.length > 0;
}

this.$el.toggleClass('hidden', !qualityProfilesVisible);
},

activate() {
const activationView = new ProfileActivationView({
rule: this.model,
collection: this.collection,
app: this.options.app
});
activationView.on('profileActivated', (severity, params, profile) => {
if (this.options.app.state.get('query').qprofile === profile) {
const activation = {
severity,
params,
inherit: 'NONE',
qProfile: profile
};
this.model.set({ activation });
}
this.refreshActives();
});
activationView.render();
},

refreshActives() {
this.options.app.controller.getRuleDetails(this.model).then(
data => {
this.collection.reset(
this.model.getInactiveProfiles(data.actives, this.options.app.qualityProfiles)
);
this.options.app.controller.updateActivation(this.model, data.actives);
},
() => {}
);
},

serializeData() {
// show "Activate" button only if user has at least one QP of the same language which he administates
const ruleLang = this.model.get('lang');
const canActivate = this.options.app.qualityProfiles.some(
profile => profile.actions && profile.actions.edit && profile.language === ruleLang
);

return {
...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
canActivate
};
}
});

+ 0
- 3
server/sonar-web/src/main/js/apps/coding-rules/styles.css View File

@@ -77,11 +77,8 @@

.coding-rules-detail-property {
display: inline-block;
vertical-align: middle;
margin-right: 20px;
font-size: var(--smallFontSize);
height: var(--controlHeight);
line-height: var(--controlHeight);
}

.coding-rules-detail-property .select2-search-field {

+ 0
- 41
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-modal.hbs View File

@@ -1,41 +0,0 @@
<form>
<div class="modal-head">
{{#eq action 'activate'}}
<h2>{{t 'coding_rules.activate_in_quality_profile'}} ({{state.total}} {{t 'coding_rules._rules'}})</h2>
{{/eq}}
{{#eq action 'deactivate'}}
<h2>{{t 'coding_rules.deactivate_in_quality_profile'}} ({{state.total}} {{t 'coding_rules._rules'}})</h2>
{{/eq}}
</div>

<div class="modal-body modal-body-select2">
<div class="js-modal-messages"></div>

<div class="modal-field">
<h3>
<label for="coding-rules-bulk-change-profile">
{{#eq action 'activate'}}{{t 'coding_rules.activate_in'}}{{/eq}}
{{#eq action 'deactivate'}}{{t 'coding_rules.deactivate_in'}}{{/eq}}
</label>
</h3>
{{#if qualityProfile}}
<h3 class="readonly-field">
{{qualityProfileName}}{{#notEq action 'change-severity'}} — {{t 'are_you_sure'}}{{/notEq}}
</h3>
{{else}}
<select id="coding-rules-bulk-change-profile" multiple>
{{#each availableQualityProfiles}}
<option value="{{key}}" {{#ifLength ../availableQualityProfiles 1}}selected{{/ifLength}}>
{{name}} - {{language}}
</option>
{{/each}}
</select>
{{/if}}
</div>
</div>

<div class="modal-foot">
<button id="coding-rules-submit-bulk-change">{{t 'apply'}}</button>
<a class="js-modal-close" href="#">{{t 'close'}}</a>
</div>
</form>

+ 0
- 41
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-popup.hbs View File

@@ -1,41 +0,0 @@
<div class="bubble-popup-title">{{t 'bulk_change'}}</div>

<ul class="bubble-popup-list">

{{! activation }}

<li>
<a class="js-bulk-change" data-action="activate">
{{t 'coding_rules.activate_in'}}&#8230;
</a>
</li>

{{#if allowActivateOnProfile}}
<li>
<a class="js-bulk-change" data-action="activate" data-param="{{qualityProfile}}">
{{t 'coding_rules.activate_in'}} <strong>{{qualityProfileName}}</strong>
</a>
</li>
{{/if}}



{{! deactivation }}

<li>
<a class="js-bulk-change" data-action="deactivate">
{{t 'coding_rules.deactivate_in'}}&#8230;
</a>
</li>

{{#if allowDeactivateOnProfile}}
<li>
<a class="js-bulk-change" data-action="deactivate" data-param="{{qualityProfile}}">
{{tp 'coding_rules.deactivate_in'}} <strong>{{qualityProfileName}}</strong>
</a>
</li>
{{/if}}
</ul>


<div class="bubble-popup-arrow"></div>

+ 0
- 20
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-layout.hbs View File

@@ -1,20 +0,0 @@
<div class="layout-page coding-rules">
<div class="layout-page-side-outer">
<div class="layout-page-side">
<div class="layout-page-side-inner">
<div class="layout-page-filters">
</div>
</div>
</div>
</div>

<div class="layout-page-main">
<div class="layout-page-header-panel layout-page-main-header">
<div class="layout-page-header-panel-inner layout-page-main-header-inner">
<div class="layout-page-main-inner coding-rules-header"></div>
</div>
</div>
<div class="layout-page-main-inner coding-rules-list"></div>
<div class="layout-page-main-inner coding-rules-details"></div>
</div>
</div>

+ 0
- 14
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-details.hbs View File

@@ -1,14 +0,0 @@
<div class="js-rule-meta"></div>
<div class="js-rule-description"></div>
<div class="js-rule-parameters"></div>

{{#if isEditable}}
<div class="coding-rules-detail-description">
<button class="js-edit-custom" id="coding-rules-detail-custom-rule-change">{{t 'edit'}}</button>
<button class="button-red js-delete" id="coding-rules-detail-rule-delete" class="button-red">{{t 'delete'}}</button>
</div>
{{/if}}

<div class="js-rule-custom-rules coding-rule-section"></div>
<div class="js-rule-profiles coding-rule-section"></div>
<div class="js-rule-issues coding-rule-section"></div>

+ 0
- 38
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-filter-form.hbs View File

@@ -1,38 +0,0 @@
<header class="menu-search">
<h6>{{t 'coding_rules.filter_similar_rules'}}</h6>
</header>

<ul class="menu">
<li>
<a href="#" class="issue-action-option" data-property="languages" data-value="{{lang}}">
{{langName}}
</a>
</li>

<li>
<a href="#" class="issue-action-option" data-property="types" data-value="{{this.type}}">
{{issueType this.type}}
</a>
</li>

{{#if severity}}
<li>
<a href="#" class="issue-action-option" data-property="severities" data-value="{{severity}}">
{{severityHelper severity}}
</a>
</li>
{{/if}}

{{#notEmpty tags}}
<li class="divider"></li>
{{#each tags}}
<li>
<a href="#" class="issue-action-option" data-property="tags" data-value="{{this}}">
<i class="icon-tags icon-half-transparent"></i>&nbsp;{{this}}
</a>
</li>
{{/each}}
{{/notEmpty}}
</ul>

<div class="bubble-popup-arrow"></div>

+ 0
- 41
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-header.hbs View File

@@ -1,41 +0,0 @@
<div class="pull-left">
{{#if state.rule}}
<a class="js-back">{{t 'coding_rules.return_to_list'}}</a>
{{else}}
{{#if canBulkChange}}
<button class="js-bulk-change">{{t 'bulk_change'}}</button>
{{/if}}
<button class="js-new-search" id="coding-rules-new-search">{{t 'clear_all_filters'}}</button>
{{/if}}
</div>


<div class="pull-right">
<span class="note big-spacer-right">
<span class="shortcut-button little-spacer-right">↑</span><span class="shortcut-button little-spacer-right">↓</span>{{t 'coding_rules.to_select_rules'}}
<span class="shortcut-button little-spacer-right big-spacer-left">←</span><span class="shortcut-button little-spacer-right">→</span>{{t 'issues.to_navigate'}}
</span>

{{#notNull state.total}}
<a class="js-reload link-no-underline" href="#">
<svg width="18" height="24" viewBox="0 0 18 24">
<path fill="#777" d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z" />
</svg>
</a>

<div class="search-navigator-header-pagination spacer-left flash flash-heavy">
<strong>
{{#gt state.total 0}}
<span class="current">
{{sum state.selectedIndex 1}}
/
<span id="coding-rules-total">{{formatMeasure state.total 'INT'}}</span>
</span>
{{else}}
<span class="current">0 / <span id="coding-rules-total">0</span></span>
{{/gt}}
</strong>
{{t 'coding_rules._rules'}}
</div>
{{/notNull}}
</div>

+ 0
- 71
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list-item.hbs View File

@@ -1,71 +0,0 @@
<table class="coding-rule-table">
<tr>
{{#if activation}}
<td class="coding-rule-table-meta-cell coding-rule-activation">
{{severityIcon activation.severity}}
{{#eq activation.inherit 'OVERRIDES'}}
<i class="icon-inheritance icon-inheritance-overridden"
title="{{tp 'coding_rules.overrides' activation.profile.name activation.parentProfile.name}}"></i>
{{/eq}}
{{#eq activation.inherit 'INHERITED'}}
<i class="icon-inheritance"
title="{{tp 'coding_rules.inherits' activation.profile.name activation.parentProfile.name}}"></i>
{{/eq}}
</td>
{{/if}}

<td>
<div class="coding-rule-title">
<a class="js-rule link-no-underline" href="{{permalink}}">{{name}}</a>
{{#if isTemplate}}
<span class="outline-badge spacer-left" title="{{t 'coding_rules.rule_template.title'}}"
data-toggle="tooltip" data-placement="bottom">{{t 'coding_rules.rule_template'}}</span>
{{/if}}
</div>
</td>

<td class="coding-rule-table-meta-cell">
<div class="coding-rule-meta">
{{#notEq status 'READY'}}
<span class="badge badge-normal-size badge-danger-light">
{{t 'rules.status' status}}
</span>
&nbsp;&nbsp;&nbsp;
{{/notEq}}
<span class="note">{{langName}}</span>
&nbsp;&nbsp;&nbsp;
<span class="note" data-toggle="tooltip" data-placement="bottom"
title="{{t 'coding_rules.type.tooltip' this.type}}">
{{issueTypeIcon this.type}} {{issueType this.type}}
</span>
{{#notEmpty tags}}
&nbsp;&nbsp;&nbsp;
<i class="icon-tags"></i>
<span class="note">{{join tags ', '}}</span>
{{/notEmpty}}
<a class="js-rule-filter link-no-underline spacer-left" href="#">
<i class="icon-filter icon-half-transparent"></i>&nbsp;<i class="icon-dropdown"></i>
</a>
</div>
</td>

{{#any activation selectedProfile}}
{{#if canEditQualityProfile}}
{{#unless isSelectedProfileBuiltIn}}
<td class="coding-rule-table-meta-cell coding-rule-activation-actions">
{{#if activation}}
<button class="coding-rules-detail-quality-profile-deactivate button-red"
{{#notEq activation.inherit 'NONE'}}disabled title="{{t 'coding_rules.can_not_deactivate'}}"{{/notEq}}>
{{t 'coding_rules.deactivate'}}
</button>
{{else}}
{{#unless isTemplate}}
<button class="coding-rules-detail-quality-profile-activate">{{t 'coding_rules.activate'}}</button>
{{/unless}}
{{/if}}
</td>
{{/unless}}
{{/if}}
{{/any}}
</tr>
</table>

+ 0
- 5
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list.hbs View File

@@ -1,5 +0,0 @@
<div class="js-list"></div>

<div class="search-navigator-workspace-list-more">
<span class="js-more"><i class="spinner"></i></span>
</div>

+ 0
- 0
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/_coding-rules-facet-header.hbs View File


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

Loading…
Cancel
Save