@@ -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; |
@@ -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); | |||
} | |||
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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 + "\"]"); | |||
} | |||
} |
@@ -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", |
@@ -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 => { |
@@ -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); | |||
} |
@@ -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); | |||
} |
@@ -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 { |
@@ -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 { |
@@ -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' | |||
} |
@@ -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"> |
@@ -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() | |||
}; | |||
} | |||
}); |
@@ -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' | |||
}; | |||
} | |||
}); |
@@ -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} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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)); |
@@ -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> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
); | |||
} | |||
}); | |||
} |
@@ -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; | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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] : []} | |||
/> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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">: </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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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 | |||
)} | |||
/> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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)] : []} | |||
/> | |||
); | |||
} | |||
} |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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 | |||
}); | |||
} |
@@ -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 | |||
}); | |||
} | |||
} | |||
} | |||
}); |
@@ -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; | |||
} | |||
}); |
@@ -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)); | |||
} | |||
}); |
@@ -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; | |||
} | |||
}); |
@@ -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 | |||
}); |
@@ -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() | |||
}; | |||
} | |||
}); |
@@ -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); | |||
} | |||
}); |
@@ -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() | |||
}; | |||
} | |||
}); |
@@ -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 | |||
}; | |||
} | |||
}); |
@@ -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()) | |||
}; | |||
} | |||
}); |
@@ -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() | |||
}; | |||
} | |||
}); |
@@ -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 }); | |||
} | |||
} | |||
}); |
@@ -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() | |||
}; | |||
} | |||
}); |
@@ -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)); | |||
} | |||
}); |
@@ -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 }; | |||
}) | |||
}; | |||
} | |||
}; | |||
} | |||
}); |
@@ -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); | |||
} | |||
}); |
@@ -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)); | |||
} | |||
}); |
@@ -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'); | |||
}; | |||
} |
@@ -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'); | |||
} | |||
}); |
@@ -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; | |||
}); | |||
} | |||
}); |
@@ -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); | |||
}); | |||
} | |||
}); |
@@ -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: {} | |||
} | |||
}); |
@@ -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; | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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')) | |||
}; | |||
} | |||
}); |
@@ -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')) | |||
}; | |||
} | |||
}); |
@@ -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'] | |||
}; | |||
} | |||
}); |
@@ -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) | |||
}; | |||
} | |||
}); |
@@ -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 | |||
}; | |||
} | |||
}); |
@@ -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'); | |||
}, | |||
() => {} | |||
); | |||
} | |||
}); |
@@ -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') | |||
}; | |||
} | |||
}); |
@@ -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 | |||
}; | |||
} | |||
}); |
@@ -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(); | |||
} | |||
}; |
@@ -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 | |||
}; | |||
} | |||
}); |
@@ -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) | |||
}; | |||
} | |||
}); |
@@ -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); | |||
} | |||
}); |
@@ -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) | |||
}; | |||
} | |||
}); |
@@ -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 | |||
}; | |||
} | |||
}); |
@@ -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 { |
@@ -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> |
@@ -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'}}… | |||
</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'}}… | |||
</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> |
@@ -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> |
@@ -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> |
@@ -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> {{this}} | |||
</a> | |||
</li> | |||
{{/each}} | |||
{{/notEmpty}} | |||
</ul> | |||
<div class="bubble-popup-arrow"></div> |
@@ -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> |
@@ -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> | |||
| |||
{{/notEq}} | |||
<span class="note">{{langName}}</span> | |||
| |||
<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}} | |||
| |||
<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> <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> |
@@ -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> |