diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-01-29 14:21:28 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-29 14:21:28 +0100 |
commit | cebce15815204aa189f63f9e1b86143b258898d2 (patch) | |
tree | 5a3a773405e86a42e29c12c3e447951052bec6e9 /server/sonar-web | |
parent | ad504279d97bd55d8c191b1ffb793c6f005ffa5a (diff) | |
download | sonarqube-cebce15815204aa189f63f9e1b86143b258898d2.tar.gz sonarqube-cebce15815204aa189f63f9e1b86143b258898d2.zip |
rewrite rules app with react (#2982)
Diffstat (limited to 'server/sonar-web')
216 files changed, 5947 insertions, 5923 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 1be2cba9f28..8089b41e562 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -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", diff --git a/server/sonar-web/src/main/js/api/issues.ts b/server/sonar-web/src/main/js/api/issues.ts index 765dcb95efb..de740e4190c 100644 --- a/server/sonar-web/src/main/js/api/issues.ts +++ b/server/sonar-web/src/main/js/api/issues.ts @@ -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 => { diff --git a/server/sonar-web/src/main/js/api/quality-profiles.ts b/server/sonar-web/src/main/js/api/quality-profiles.ts index 80dd491a75e..188651ddc31 100644 --- a/server/sonar-web/src/main/js/api/quality-profiles.ts +++ b/server/sonar-web/src/main/js/api/quality-profiles.ts @@ -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); +} diff --git a/server/sonar-web/src/main/js/api/rules.ts b/server/sonar-web/src/main/js/api/rules.ts index 1a29eb863e8..ea42bf0a1a7 100644 --- a/server/sonar-web/src/main/js/api/rules.ts +++ b/server/sonar-web/src/main/js/api/rules.ts @@ -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); +} diff --git a/server/sonar-web/src/main/js/app/styles/components/search-navigator.css b/server/sonar-web/src/main/js/app/styles/components/search-navigator.css index f8b21277653..c997af3dae1 100644 --- a/server/sonar-web/src/main/js/app/styles/components/search-navigator.css +++ b/server/sonar-web/src/main/js/app/styles/components/search-navigator.css @@ -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 { diff --git a/server/sonar-web/src/main/js/app/styles/init/forms.css b/server/sonar-web/src/main/js/app/styles/init/forms.css index 9abdbddf828..beb0204de27 100644 --- a/server/sonar-web/src/main/js/app/styles/init/forms.css +++ b/server/sonar-web/src/main/js/app/styles/init/forms.css @@ -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 { diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index dedfabe68c2..ef1f97b8cf1 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -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' +} diff --git a/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js b/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js index 40b5093a8ea..a4848285d9d 100644 --- a/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js +++ b/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/bulk-change-modal-view.js b/server/sonar-web/src/main/js/apps/coding-rules/bulk-change-modal-view.js deleted file mode 100644 index 1708a0c6887..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/bulk-change-modal-view.js +++ /dev/null @@ -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() - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/bulk-change-popup-view.js b/server/sonar-web/src/main/js/apps/coding-rules/bulk-change-popup-view.js deleted file mode 100644 index 00b686f7f7c..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/bulk-change-popup-view.js +++ /dev/null @@ -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' - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx new file mode 100644 index 00000000000..b96a2513843 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx @@ -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} + /> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx new file mode 100644 index 00000000000..2fcc083b688 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx new file mode 100644 index 00000000000..d9dcbf764cc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx new file mode 100644 index 00000000000..2afc90bae9c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx new file mode 100644 index 00000000000..b0a18157f67 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx new file mode 100644 index 00000000000..37efe2e266e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx @@ -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} + /> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx new file mode 100644 index 00000000000..54368071b6b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js b/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js deleted file mode 100644 index 3ec7c428785..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js +++ /dev/null @@ -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)); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx new file mode 100644 index 00000000000..775fe1a560f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx @@ -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> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.tsx new file mode 100644 index 00000000000..b2e9c8d5eb6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.tsx @@ -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} + /> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx new file mode 100644 index 00000000000..b650c27fd43 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/status-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx index ced74adef4a..e9b7ef5c20c 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/status-facet.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx @@ -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} + /> + ); } -}); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx new file mode 100644 index 00000000000..dd0a053202e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx new file mode 100644 index 00000000000..e384fcb6e16 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/InheritanceFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/InheritanceFacet.tsx new file mode 100644 index 00000000000..c4c1102db07 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/InheritanceFacet.tsx @@ -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] : []} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx new file mode 100644 index 00000000000..057d51961f8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx new file mode 100644 index 00000000000..34b27b53aeb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/PageActions.tsx new file mode 100644 index 00000000000..3341efb884c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/PageActions.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx new file mode 100644 index 00000000000..402b971dcaa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx new file mode 100644 index 00000000000..95a7ee29f44 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx new file mode 100644 index 00000000000..a32e7293c26 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx new file mode 100644 index 00000000000..d9c9007287f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx new file mode 100644 index 00000000000..9d7cd528167 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx new file mode 100644 index 00000000000..c5e31bcb5a5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx new file mode 100644 index 00000000000..78b71bb74e7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx new file mode 100644 index 00000000000..3728d1e8c0e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx new file mode 100644 index 00000000000..a7385653070 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx new file mode 100644 index 00000000000..f8dce629e1d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx new file mode 100644 index 00000000000..895ab60771f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleInheritanceIcon.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleInheritanceIcon.tsx new file mode 100644 index 00000000000..264d79f7cc2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleInheritanceIcon.tsx @@ -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 + )} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx new file mode 100644 index 00000000000..2c97ea8e946 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx new file mode 100644 index 00000000000..ea93d9559a1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/facet/FacetFooter.js b/server/sonar-web/src/main/js/apps/coding-rules/components/StatusFacet.tsx index 6e67ec9f302..79752f33732 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetFooter.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/StatusFacet.tsx @@ -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} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx new file mode 100644 index 00000000000..9ca37cb5de4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx new file mode 100644 index 00000000000..4a5c87d4774 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx @@ -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)] : []} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js b/server/sonar-web/src/main/js/apps/coding-rules/components/TypeFacet.tsx index d7f150fb577..8094cf54b33 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/TypeFacet.tsx @@ -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} + /> + ); } } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/confirm-dialog.js b/server/sonar-web/src/main/js/apps/coding-rules/confirm-dialog.js deleted file mode 100644 index bb9f70c87c4..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/confirm-dialog.js +++ /dev/null @@ -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 - }); -} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/controller.js b/server/sonar-web/src/main/js/apps/coding-rules/controller.js deleted file mode 100644 index d1d84a4b0f4..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/controller.js +++ /dev/null @@ -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 - }); - } - } - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets-view.js b/server/sonar-web/src/main/js/apps/coding-rules/facets-view.js deleted file mode 100644 index 2b38da26600..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets-view.js +++ /dev/null @@ -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; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/active-severity-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/active-severity-facet.js deleted file mode 100644 index 124a1a80d41..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/active-severity-facet.js +++ /dev/null @@ -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)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/available-since-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/available-since-facet.js deleted file mode 100644 index 1eec5526e27..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/available-since-facet.js +++ /dev/null @@ -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; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/base-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/base-facet.js deleted file mode 100644 index b87d88d048c..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/base-facet.js +++ /dev/null @@ -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 -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/custom-labels-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/custom-labels-facet.js deleted file mode 100644 index 903a1722c04..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/custom-labels-facet.js +++ /dev/null @@ -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() - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/custom-values-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/custom-values-facet.js deleted file mode 100644 index a5dd6c6f4fe..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/custom-values-facet.js +++ /dev/null @@ -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); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/inheritance-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/inheritance-facet.js deleted file mode 100644 index 805f08a95c2..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/inheritance-facet.js +++ /dev/null @@ -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() - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/key-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/key-facet.js deleted file mode 100644 index 7f8615ab304..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/key-facet.js +++ /dev/null @@ -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 - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/language-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/language-facet.js deleted file mode 100644 index 5ab14fe86b8..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/language-facet.js +++ /dev/null @@ -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()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/quality-profile-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/quality-profile-facet.js deleted file mode 100644 index 27e33969ba2..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/quality-profile-facet.js +++ /dev/null @@ -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() - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js deleted file mode 100644 index 3f7507847c2..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js +++ /dev/null @@ -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 }); - } - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/repository-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/repository-facet.js deleted file mode 100644 index a7c91d55928..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/repository-facet.js +++ /dev/null @@ -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() - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/severity-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/severity-facet.js deleted file mode 100644 index b361118e3f1..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/severity-facet.js +++ /dev/null @@ -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)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/tag-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/tag-facet.js deleted file mode 100644 index 59bfc9a250f..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/tag-facet.js +++ /dev/null @@ -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 }; - }) - }; - } - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/template-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/template-facet.js deleted file mode 100644 index ffd1bbea452..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/template-facet.js +++ /dev/null @@ -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); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets/type-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/type-facet.js deleted file mode 100644 index d3eeaac0671..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets/type-facet.js +++ /dev/null @@ -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)); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/init.js b/server/sonar-web/src/main/js/apps/coding-rules/init.js deleted file mode 100644 index 2850482373e..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/init.js +++ /dev/null @@ -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'); - }; -} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/layout.js b/server/sonar-web/src/main/js/apps/coding-rules/layout.js deleted file mode 100644 index 5b5b51ab644..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/layout.js +++ /dev/null @@ -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'); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/models/rule.js b/server/sonar-web/src/main/js/apps/coding-rules/models/rule.js deleted file mode 100644 index 1f418109921..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/models/rule.js +++ /dev/null @@ -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; - }); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/models/rules.js b/server/sonar-web/src/main/js/apps/coding-rules/models/rules.js deleted file mode 100644 index 29c06c8904e..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/models/rules.js +++ /dev/null @@ -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); - }); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/models/state.js b/server/sonar-web/src/main/js/apps/coding-rules/models/state.js deleted file mode 100644 index f4786b66cd1..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/models/state.js +++ /dev/null @@ -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: {} - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/query.ts b/server/sonar-web/src/main/js/apps/coding-rules/query.ts new file mode 100644 index 00000000000..c893b5f1d77 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/query.ts @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/routes.ts b/server/sonar-web/src/main/js/apps/coding-rules/routes.ts index 354feca37a6..260a9db516a 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/routes.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/routes.ts @@ -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)); } } } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js deleted file mode 100644 index 93150765a72..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js +++ /dev/null @@ -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')) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule-filter-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule-filter-view.js deleted file mode 100644 index 4dfd1102402..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule-filter-view.js +++ /dev/null @@ -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')) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js deleted file mode 100644 index 515b292fa8b..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js +++ /dev/null @@ -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'] - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js deleted file mode 100644 index 072347af7ab..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js +++ /dev/null @@ -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) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js deleted file mode 100644 index 711699ad0e9..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js +++ /dev/null @@ -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 - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/delete-rule-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/delete-rule-view.js deleted file mode 100644 index d26d168a704..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/delete-rule-view.js +++ /dev/null @@ -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'); - }, - () => {} - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js deleted file mode 100644 index c90a93d19ab..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js +++ /dev/null @@ -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') - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-description-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-description-view.js deleted file mode 100644 index a7b693e8ed1..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-description-view.js +++ /dev/null @@ -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 - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js deleted file mode 100644 index 4294b4717e9..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js +++ /dev/null @@ -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(); - } -}; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js deleted file mode 100644 index 297057aec1f..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js +++ /dev/null @@ -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 - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js deleted file mode 100644 index c787635455b..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js +++ /dev/null @@ -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) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-parameters-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-parameters-view.js deleted file mode 100644 index a140467330c..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-parameters-view.js +++ /dev/null @@ -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); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profile-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profile-view.js deleted file mode 100644 index 176a36f9778..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profile-view.js +++ /dev/null @@ -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) - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js deleted file mode 100644 index 0b8122e5e31..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js +++ /dev/null @@ -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 - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/styles.css b/server/sonar-web/src/main/js/apps/coding-rules/styles.css index eb42a097565..ceef209eeea 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/styles.css +++ b/server/sonar-web/src/main/js/apps/coding-rules/styles.css @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-modal.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-modal.hbs deleted file mode 100644 index 8816b6cb5af..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-modal.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-popup.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-popup.hbs deleted file mode 100644 index c5a4622e9f6..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-popup.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-layout.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-layout.hbs deleted file mode 100644 index 51efa6c6f8b..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-layout.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-details.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-details.hbs deleted file mode 100644 index 9df22aef867..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-details.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-filter-form.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-filter-form.hbs deleted file mode 100644 index 20076a4b374..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-filter-form.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-header.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-header.hbs deleted file mode 100644 index 900951bc70d..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-header.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list-item.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list-item.hbs deleted file mode 100644 index 4f87b2c55de..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list-item.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list.hbs deleted file mode 100644 index cae9c964a0b..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list.hbs +++ /dev/null @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/_coding-rules-facet-header.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/_coding-rules-facet-header.hbs deleted file mode 100644 index 26ba41a27b3..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/_coding-rules-facet-header.hbs +++ /dev/null @@ -1,6 +0,0 @@ -<span class="search-navigator-facet-header"> - <a class="js-facet-toggle"> - <i class="icon-checkbox {{#if enabled}}icon-checkbox-checked{{/if}}"></i> - {{t 'coding_rules.facet' property}} - </a> -</span> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-available-since-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-available-since-facet.hbs deleted file mode 100644 index db6138c3663..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-available-since-facet.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-container"> - <input type="text" class="search-navigator-facet-input" name="availableSince" placeholder="{{t 'date'}}"> -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-base-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-base-facet.hbs deleted file mode 100644 index c9b5d3d034a..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-base-facet.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}"> - <span class="facet-name">{{default label val}}{{#if extra}} <span class="note">{{extra}}</span>{{/if}}</span> - <span class="facet-stat">{{formatMeasure count 'SHORT_INT'}}</span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-custom-values-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-custom-values-facet.hbs deleted file mode 100644 index f527fac7108..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-custom-values-facet.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}"> - <span class="facet-name">{{default label val}}{{#if extra}} <span class="note">{{extra}}</span>{{/if}}</span> - <span class="facet-stat">{{formatMeasure count 'SHORT_INT'}}</span> - </a> - {{/each}} - - <div class="search-navigator-facet-custom-value"> - <input type="hidden" class="js-custom-value"> - </div> -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-inheritance-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-inheritance-facet.hbs deleted file mode 100644 index b774078c6bb..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-inheritance-facet.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}"> - <span class="facet-name">{{default label val}}{{#if extra}} <span class="note">{{extra}}</span>{{/if}}</span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-key-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-key-facet.hbs deleted file mode 100644 index 1cc7b5bceed..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-key-facet.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-container"> - {{key}} -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-quality-profile-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-quality-profile-facet.hbs deleted file mode 100644 index 23caeed013d..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-quality-profile-facet.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}"> - <span class="facet-name">{{default label val}}{{#if extra}} <span class="note">{{extra}}</span>{{/if}}{{#if isBuiltIn}} <span class="note">({{t 'quality_profiles.built_in'}})</span>{{/if}}</span> - <span class="facet-stat"> - <span class="js-active facet-toggle facet-toggle-green {{#if ../toggled}}facet-toggle-active{{/if}}">active</span> - <span class="js-inactive facet-toggle facet-toggle-red {{#unless ../toggled}}facet-toggle-active{{/unless}}">inactive</span> - </span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-query-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-query-facet.hbs deleted file mode 100644 index c8f3d84d5b1..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-query-facet.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="search-navigator-facet-query"> - <form class="search-box"> - <input class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_for_rules'}}" maxlength="100"> - <svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"> - <g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)"> - <path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/> - </g> - </svg> - <button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset"> - <svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"> - <path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/> - </svg> - </button> - <span class="js-hint search-box-note hidden" title="{{tp 'select2.tooShort' 2}}"> - {{tp 'select2.tooShort' 2}} - </span> - </form> -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-severity-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-severity-facet.hbs deleted file mode 100644 index 972a2608f87..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-severity-facet.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet search-navigator-facet-half js-facet" data-value="{{val}}" title="{{t 'severity' val}}"> - <span class="facet-name">{{severityIcon val}} {{t 'severity' val}}</span> - <span class="facet-stat">{{formatMeasure count 'SHORT_INT'}}</span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-template-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-template-facet.hbs deleted file mode 100644 index ef7dd5b98f2..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-template-facet.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-list"> - <a class="facet search-navigator-facet js-facet" data-value="true"> - <span class="facet-name">{{t 'coding_rules.filters.template.is_template'}}</span> - <span class="facet-stat"></span> - </a> - <a class="facet search-navigator-facet js-facet" data-value="false"> - <span class="facet-name">{{t 'coding_rules.filters.template.is_not_template'}}</span> - <span class="facet-stat"></span> - </a> -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-type-facet.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-type-facet.hbs deleted file mode 100644 index cfd295f60cf..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-type-facet.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{> '_coding-rules-facet-header'}} - -<div class="search-navigator-facet-list"> - {{#each values}} - <a class="facet search-navigator-facet js-facet" - data-value="{{val}}"> - <span class="facet-name">{{issueTypeIcon val}} {{t 'issue.type' val}}</span> - <span class="facet-stat">{{formatMeasure count 'SHORT_INT'}}</span> - </a> - {{/each}} -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule-creation.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule-creation.hbs deleted file mode 100644 index 389522453af..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule-creation.hbs +++ /dev/null @@ -1,96 +0,0 @@ -<form> - <div class="modal-head"> - {{#if change}} - <h2>{{t 'coding_rules.update_custom_rule'}}</h2> - {{else}} - <h2>{{t 'coding_rules.create_custom_rule'}}</h2> - {{/if}} - </div> - - <div class="modal-body modal-container"> - <div class="js-modal-messages"></div> - - <table> - <tr class="property"> - <th class="nowrap"><h3>{{t 'name'}} <em class="mandatory">*</em></h3></th> - <td> - <input type="text" name="name" id="coding-rules-custom-rule-creation-name" - class="coding-rules-name-key" value="{{name}}"/> - </td> - </tr> - <tr class="property"> - <th class="nowrap"><h3>{{t 'key'}}{{#unless change}} <em class="mandatory">*</em>{{/unless}}</h3></th> - <td> - {{#if change}} - <span class="coding-rules-detail-custom-rule-key" title="{{key}}">{{key}}</span> - {{else}} - <input type="text" name="key" id="coding-rules-custom-rule-creation-key" - class="coding-rules-name-key" value="{{internalKey}}"/> - {{/if}} - </td> - </tr> - <tr class="property"> - <th class="nowrap"><h3>{{t 'description'}} <em class="mandatory">*</em></h3></th> - <td> - <textarea name="markdown_description" id="coding-rules-custom-rule-creation-html-description" - class="coding-rules-markdown-description" rows="15">{{{mdDesc}}}</textarea> - <span class="text-right">{{> '../../../../components/common/templates/_markdown-tips' }}</span> - </td> - </tr> - <tr class="property"> - <th class="nowrap"><h3>{{t 'type'}}</h3></th> - <td> - <select id="coding-rules-custom-rule-creation-type"> - {{#each types}} - <option value="{{this}}">{{t 'issue.type' this}}</option> - {{/each}} - </select> - </td> - </tr> - <tr class="property"> - <th class="nowrap"><h3>{{t 'severity'}}</h3></th> - <td> - <select id="coding-rules-custom-rule-creation-severity"> - {{#each severities}} - <option value="{{this}}">{{t 'severity' this}}</option> - {{/each}} - </select> - </td> - </tr> - <tr class="property"> - <th class="nowrap"><h3>{{t 'coding_rules.filters.status'}}</h3></th> - <td> - <select id="coding-rules-custom-rule-creation-status"> - {{#each statuses}} - <option value="{{id}}">{{text}}</option> - {{/each}} - </select> - </td> - </tr> - {{#each params}} - <tr class="property"> - <th class="nowrap"><h3>{{key}}</h3></th> - <td> - {{#eq type 'TEXT'}} - <textarea class="width100" rows="3" name="{{key}}" placeholder="{{defaultValue}}">{{value}}</textarea> - {{else}} - <input type="text" name="{{key}}" value="{{value}}" placeholder="{{defaultValue}}"/> - {{/eq}} - <div class="note">{{{htmlDesc}}}</div> - {{#if extra}} - <div class="note">{{extra}}</div> - {{/if}} - </td> - </tr> - {{/each}} - </table> - </div> - - <div class="modal-foot"> - <button id="coding-rules-custom-rule-creation-create"> - {{#if change}}{{t 'save'}}{{else}}{{t 'create'}}{{/if}} - </button> - <button id="coding-rules-custom-rule-creation-reactivate" class="hidden">{{t 'coding_rules.reactivate'}}</button> - <a id="coding-rules-custom-rule-creation-cancel">{{t 'cancel'}}</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule.hbs deleted file mode 100644 index 32fff8638c6..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule.hbs +++ /dev/null @@ -1,26 +0,0 @@ -<td class="coding-rules-detail-list-name"> - <a href="{{permalink}}">{{name}}</a> -</td> - -<td class="coding-rules-detail-list-severity"> - {{severityIcon severity}} {{t "severity" severity}} -</td> - -<td class="coding-rules-detail-list-parameters"> - {{#each params}} - {{#if defaultValue}} - <div class="coding-rules-detail-list-parameter"> - <span class="key">{{key}}</span><span class="sep">: </span><span class="value" title="{{value}}">{{defaultValue}}</span> - </div> - {{/if}} - {{/each}} - -</td> - -{{#if canDeleteCustomRule}} -<td class="coding-rules-detail-list-actions"> - <button class="js-delete-custom-rule button-red"> - {{t 'delete'}} - </button> -</td> -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rules.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rules.hbs deleted file mode 100644 index 402c99a16c8..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rules.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div class="coding-rules-detail-custom-rules-section"> - <div class="coding-rule-section-separator"></div> - - <h3 class="coding-rules-detail-title">{{t 'coding_rules.custom_rules'}}</h3> - - {{#if canCreateCustomRule}} - <button class="js-create-custom-rule spacer-left">{{t 'coding_rules.create'}}</button> - {{/if}} - - <table id="coding-rules-detail-custom-rules" class="coding-rules-detail-list"></table> -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-delete-rule.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-delete-rule.hbs deleted file mode 100644 index 4c9a7f3fbf9..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-delete-rule.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<form> - <div class="modal-head"> - <h2>{{t 'coding_rules.delete_rule'}}</h2> - </div> - - <div class="modal-body"> - {{tp 'coding_rules.delete.custom.confirm' name}} - </div> - - <div class="modal-foot"> - <button className="button-red">{{t 'delete'}}</button> - <a class="js-modal-close">{{t 'cancel'}}</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-profile-activation.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-profile-activation.hbs deleted file mode 100644 index e2c87370259..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-profile-activation.hbs +++ /dev/null @@ -1,78 +0,0 @@ -<form> - <div class="modal-head"> - {{#if change}} - <h2>{{t 'coding_rules.change_details'}}</h2> - {{else}} - <h2>{{t 'coding_rules.activate_in_quality_profile'}}</h2> - {{/if}} - </div> - - <div class="modal-body modal-container"> - <div class="js-modal-messages"></div> - - {{#empty qualityProfiles}} - {{#unless change}} - <div class="alert alert-info">{{t 'coding_rules.active_in_all_profiles'}}</div> - {{/unless}} - {{/empty}} - - <div class="modal-field"> - <label>{{t 'coding_rules.quality_profile'}}</label> - {{#any key qProfile}} - {{name}} - {{else}} - <select id="coding-rules-quality-profile-activation-select"> - {{#each qualityProfiles}} - <option value="{{key}}" {{#eq key ../contextProfile}}selected{{/eq}}> - {{#repeat depth}} {{/repeat}}{{name}} - </option> - {{/each}} - </select> - {{/any}} - </div> - <div class="modal-field"> - <label>{{t 'severity'}}</label> - <select id="coding-rules-quality-profile-activation-severity"> - {{#each severities}} - <option value="{{this}}">{{t 'severity' this}}</option> - {{/each}} - </select> - </div> - {{#if isCustomRule}} - <div class="modal-field"> - <p class="note">{{t 'coding_rules.custom_rule.activation_notice'}}</p> - </div> - {{else}} - {{#each params}} - <div class="modal-field"> - <label title="{{key}}">{{key}}</label> - {{#eq type 'TEXT'}} - <textarea class="width100" rows="3" name="{{key}}" placeholder="{{defaultValue}}">{{value}}</textarea> - {{else}} - {{#eq type 'BOOLEAN'}} - <select name="{{key}}" value="{{value}}"> - <option value="{{defaultValue}}">{{t 'default'}} ({{t defaultValue}})</option> - <option value="true"{{#eq value 'true'}} selected="selected"{{/eq}}>{{t 'true'}}</option> - <option value="false"{{#eq value 'false'}} selected="selected"{{/eq}}>{{t 'false'}}</option> - </select> - {{else}} - <input type="text" name="{{key}}" value="{{value}}" placeholder="{{defaultValue}}"> - {{/eq}} - {{/eq}} - <div class="modal-field-description">{{{htmlDesc}}}</div> - {{#if extra}} - <div class="modal-field-description">{{extra}}</div> - {{/if}} - </div> - {{/each}} - {{/if}} - </div> - - <div class="modal-foot"> - <button id="coding-rules-quality-profile-activation-activate" - {{#unless saveEnabled}}disabled="disabled"{{/unless}}> - {{#if change}}{{t 'save'}}{{else}}{{t 'coding_rules.activate'}}{{/if}} - </button> - <a id="coding-rules-quality-profile-activation-cancel" class="js-modal-close">{{t 'cancel'}}</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-description.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-description.hbs deleted file mode 100644 index 17b3e986141..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-description.hbs +++ /dev/null @@ -1,41 +0,0 @@ -<div class="coding-rules-detail-description rule-desc markdown">{{{htmlDesc}}}</div> - -{{#unless isCustom}} - <div class="coding-rules-detail-description coding-rules-detail-description-extra"> - <div id="coding-rules-detail-description-extra"> - {{#if htmlNote}} - <div class="rule-desc spacer-bottom markdown">{{{htmlNote}}}</div> - {{/if}} - {{#if canCustomizeRule}} - <button id="coding-rules-detail-extend-description">{{t 'coding_rules.extend_description'}}</button> - {{/if}} - </div> - - {{#if canCustomizeRule}} - <div class="coding-rules-detail-extend-description-form hidden"> - <table class="width100"> - <tbody> - <tr> - <td class="width100" colspan="2"> - <textarea id="coding-rules-detail-extend-description-text" rows="4" - style="width: 100%; margin-bottom: 4px;">{{mdNote}}</textarea> - </td> - </tr> - <tr> - <td> - <button id="coding-rules-detail-extend-description-submit">{{t 'save'}}</button> - {{#if mdNote}} - <button id="coding-rules-detail-extend-description-remove" class="button-red">{{t 'remove'}}</button> - {{/if}} - <a id="coding-rules-detail-extend-description-cancel" class="spacer-left">{{t 'cancel'}}</a> - </td> - <td class="text-right"> - {{> '../../../../components/common/templates/_markdown-tips' }} - </td> - </tr> - </tbody> - </table> - </div> - {{/if}} - </div> -{{/unless}} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-issues.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-issues.hbs deleted file mode 100644 index 32f822b85ff..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-issues.hbs +++ /dev/null @@ -1,27 +0,0 @@ -<div class="coding-rule-section-separator"></div> - -{{#if loading}} - <h3 class="coding-rules-detail-title"> - {{t 'coding_rules.issues'}} <i class="spinner spacer-left"/> - </h3> -{{else}} - <h3 class="coding-rules-detail-title"> - {{t 'coding_rules.issues'}} (<a href="{{totalIssuesUrl}}">{{total}}</a>) - </h3> - - {{#notEmpty projects}} - <table class="coding-rules-detail-list coding-rules-most-violated-projects"> - <tr> - <td class="coding-rules-detail-list-name" colspan="2">{{t 'coding_rules.most_violating_projects'}}</td> - </tr> - {{#each projects}} - <tr> - <td class="coding-rules-detail-list-name">{{name}}</td> - <td class="coding-rules-detail-list-parameters"> - <a href="{{issuesUrl}}" target="_blank">{{count}}</a> - </td> - </tr> - {{/each}} - </table> - {{/notEmpty}} -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-meta.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-meta.hbs deleted file mode 100644 index 4483e6e034f..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-meta.hbs +++ /dev/null @@ -1,95 +0,0 @@ -<header class="page-header"> - <div class="page-actions"> - <span class="note">{{key}}</span> - - <a class="coding-rules-detail-permalink link-no-underline spacer-left" target="_blank" href="{{permalink}}"> - <svg - class="text-text-top" - xmlns="http://www.w3.org/2000/svg" - height=14 - width=14 - viewBox="0 0 16 16"> - <path - fill="currentColor" - d="M13.501 11.429q0-0.357-0.25-0.607l-1.857-1.857q-0.25-0.25-0.607-0.25-0.375 0-0.643 0.286 0.027 0.027 0.17 0.165t0.192 0.192 0.134 0.17 0.116 0.228 0.031 0.246q0 0.357-0.25 0.607t-0.607 0.25q-0.134 0-0.246-0.031t-0.228-0.116-0.17-0.134-0.192-0.192-0.165-0.17q-0.295 0.277-0.295 0.652 0 0.357 0.25 0.607l1.839 1.848q0.241 0.241 0.607 0.241 0.357 0 0.607-0.232l1.313-1.304q0.25-0.25 0.25-0.598zM7.224 5.134q0-0.357-0.25-0.607l-1.839-1.848q-0.25-0.25-0.607-0.25-0.348 0-0.607 0.241l-1.313 1.304q-0.25 0.25-0.25 0.598 0 0.357 0.25 0.607l1.857 1.857q0.241 0.241 0.607 0.241 0.375 0 0.643-0.277-0.027-0.027-0.17-0.165t-0.192-0.192-0.134-0.17-0.116-0.228-0.031-0.246q0-0.357 0.25-0.607t0.607-0.25q0.134 0 0.246 0.031t0.228 0.116 0.17 0.134 0.192 0.192 0.165 0.17q0.295-0.277 0.295-0.652zM15.215 11.429q0 1.071-0.759 1.813l-1.313 1.304q-0.741 0.741-1.813 0.741-1.080 0-1.821-0.759l-1.839-1.848q-0.741-0.741-0.741-1.813 0-1.098 0.786-1.866l-0.786-0.786q-0.768 0.786-1.857 0.786-1.071 0-1.821-0.75l-1.857-1.857q-0.75-0.75-0.75-1.821t0.759-1.813l1.313-1.304q0.741-0.741 1.813-0.741 1.080 0 1.821 0.759l1.839 1.848q0.741 0.741 0.741 1.813 0 1.098-0.786 1.866l0.786 0.786q0.768-0.786 1.857-0.786 1.071 0 1.821 0.75l1.857 1.857q0.75 0.75 0.75 1.821z" - /> - </svg> - </a> - - <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> - <h3 class="page-title coding-rules-detail-header"> - <big>{{name}}</big> - </h3> -</header> - -<ul class="coding-rules-detail-properties"> - <li class="coding-rules-detail-property" - data-toggle="tooltip" data-placement="bottom" title="{{t 'coding_rules.type.tooltip' this.type}}"> - {{issueTypeIcon this.type}} {{issueType this.type}} - </li> - - <li class="coding-rules-detail-property" - data-toggle="tooltip" data-placement="bottom" title="{{t 'default_severity'}}"> - {{severityIcon severity}} {{t "severity" severity}} - </li> - - {{#notEq status 'READY'}} - <li class="coding-rules-detail-property" - data-toggle="tooltip" data-placement="bottom" title="{{t 'status'}}"> - <span class="badge badge-normal-size badge-danger-light"> - {{t 'rules.status' status}} - </span> - </li> - {{/notEq}} - - <li class="coding-rules-detail-property coding-rules-detail-tag-list {{#if canCustomizeRule}}coding-rules-detail-tags-change{{/if}}" - data-toggle="tooltip" data-placement="bottom" title="{{t 'tags'}}"> - <i class="icon-tags"></i> - <span>{{#if allTags}}{{join allTags ', '}}{{else}}{{t 'coding_rules.no_tags'}}{{/if}}</span> - {{#if canCustomizeRule}}<i class="icon-dropdown"></i>{{/if}} - </li> - - {{#if canCustomizeRule}} - <li class="coding-rules-detail-property coding-rules-detail-tag-edit hidden"> - {{#if sysTags}}<i class="icon-tags"></i> - <span>{{join sysTags ', '}}</span>{{/if}} - <input class="coding-rules-detail-tag-input" type="text" value="{{#if tags}}{{join tags ','}}{{/if}}"> - - <button class="coding-rules-detail-tag-edit-done text-middle">{{t 'Done'}}</button> - <a class="coding-rules-details-tag-edit-cancel spacer-left">{{t 'cancel'}}</a> - </li> - {{/if}} - - <li class="coding-rules-detail-property">{{t 'coding_rules.available_since'}} {{d createdAt}}</li> - - <li class="coding-rules-detail-property" - data-toggle="tooltip" data-placement="bottom" title="Rule repository (language)"> - {{repoName}} ({{langName}}) - </li> - - {{#if isTemplate}} - <li class="coding-rules-detail-property" - title="{{t 'coding_rules.rule_template.title'}}">{{t 'coding_rules.rule_template'}}</li> - {{/if}} - - {{#if templateKey}} - <li class="coding-rules-detail-property" - title="{{t 'coding_rules.custom_rule.title'}}">{{t 'coding_rules.custom_rule'}} - (<a href="#rule_key={{templateKey}}">{{t 'coding_rules.show_template'}}</a>) - </li> - {{/if}} - - {{#if debtRemFnType}} - <li class="coding-rules-detail-property" - data-toggle="tooltip" data-placement="bottom" title="{{t 'coding_rules.remediation_function'}}"> - {{t 'coding_rules.remediation_function' debtRemFnType}}: - - {{#if debtRemFnOffset}}{{debtRemFnOffset}}{{/if}} - {{#if debtRemFnCoeff}}{{#if debtRemFnOffset}}+{{/if}}{{debtRemFnCoeff}}{{/if}} - {{#if effortToFixDescription}}{{effortToFixDescription}}{{/if}} - </li> - {{/if}} -</ul> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-parameters.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-parameters.hbs deleted file mode 100644 index 0298c2aa33e..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-parameters.hbs +++ /dev/null @@ -1,27 +0,0 @@ -<h3 class="coding-rules-detail-title">{{t 'coding_rules.parameters'}}</h3> -<table class="coding-rules-detail-parameters"> - {{#each params}} - <tr class="coding-rules-detail-parameter"> - <td class="coding-rules-detail-parameter-name">{{key}}</td> - <td class="coding-rules-detail-parameter-description" data-key="{{key}}"> - <p>{{{htmlDesc}}}</p> - {{#if ../../templateKey}} - <div class="note spacer-top"> - {{#if defaultValue }} - <span class="coding-rules-detail-parameter-value">{{defaultValue}}</span> - {{else}} - {{t 'coding_rules.parameter.empty'}} - {{/if}} - </div> - {{else}} - {{#if defaultValue}} - <div class="note spacer-top"> - {{t 'coding_rules.parameters.default_value'}}<br> - <span class="coding-rules-detail-parameter-value">{{defaultValue}}</span> - </div> - {{/if}} - {{/if}} - </td> - </tr> - {{/each}} -</table> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profile.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profile.hbs deleted file mode 100644 index c0b2bfe0619..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profile.hbs +++ /dev/null @@ -1,82 +0,0 @@ -<td class="coding-rules-detail-quality-profile-name"> - <a href="{{profilePath}}"> - {{name}} - </a> - {{#if isBuiltIn}} - <span class="outline-badge spacer-left" data-toggle="tooltip" data-placement="bottom" - title="{{t 'quality_profiles.built_in.description.1'}} {{t 'quality_profiles.built_in.description.2'}}"> - {{t 'quality_profiles.built_in'}} - </span> - {{/if}} - {{#if parent}} - <div class="coding-rules-detail-quality-profile-inheritance"> - {{#eq inherit 'OVERRIDES'}} - <i class="icon-inheritance icon-inheritance-overridden" title="{{tp 'coding_rules.overrides' name parent.name}}"></i> - {{/eq}} - {{#eq inherit 'INHERITED'}} - <i class="icon-inheritance" title="{{tp 'coding_rules.inherits' name parent.name}}"></i> - {{/eq}} - <a class="link-base-color" href="{{parentProfilePath}}"> - {{parent.name}} - </a> - </div> - {{/if}} -</td> - -{{#if severity}} - <td class="coding-rules-detail-quality-profile-severity"> - <span data-toggle="tooltip" data-placement="bottom" title="Activation severity"> - {{severityIcon severity}} {{t "severity" severity}} - </span> - {{#if parent}}{{#notEq severity parent.severity}} - <div class="coding-rules-detail-quality-profile-inheritance"> - {{t 'coding_rules.original'}} {{t 'severity' parent.severity}} - </div> - {{/notEq}}{{/if}} - </td> - - {{#unless templateKey}} - <td class="coding-rules-detail-quality-profile-parameters"> - {{#each parameters}} - <div class="coding-rules-detail-quality-profile-parameter"> - <span class="key">{{key}}</span><span class="sep">: </span><span class="value" - title="{{value}}">{{value}}</span> - {{#if ../parent}}{{#notEq value original}} - <div class="coding-rules-detail-quality-profile-inheritance"> - {{t 'coding_rules.original'}} <span class="value">{{original}}</span> - </div> - {{/notEq}}{{/if}} - </div> - {{/each}} - - </td> - {{/unless}} - - <td class="coding-rules-detail-quality-profile-actions"> - {{#if actions.edit}} - {{#unless isBuiltIn}} - {{#unless isTemplate}} - <button class="coding-rules-detail-quality-profile-change">{{t 'change_verb'}}</button> - {{/unless}} - {{#if parent}} - {{#eq inherit 'OVERRIDES'}} - <button class="coding-rules-detail-quality-profile-revert button-red"> - {{t 'coding_rules.revert_to_parent_definition'}} - </button> - {{/eq}} - {{else}} - <button class="coding-rules-detail-quality-profile-deactivate button-red"> - {{t 'coding_rules.deactivate'}} - </button> - {{/if}} - {{/unless}} - {{/if}} - </td> - -{{else}} - {{#if canWrite}}{{#unless isTemplate}} - <td class="coding-rules-detail-quality-profile-actions"> - <button class="coding-rules-detail-quality-profile-activate">{{t 'coding_rules.activate'}}</button> - </td> - {{/unless}}{{/if}} -{{/if}} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profiles.hbs b/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profiles.hbs deleted file mode 100644 index 44131fc2f28..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profiles.hbs +++ /dev/null @@ -1,19 +0,0 @@ -<div class="coding-rules-detail-quality-profiles-section"> - <div class="coding-rule-section-separator"></div> - - <h3 class="coding-rules-detail-title">{{t 'coding_rules.quality_profiles'}}</h3> - - {{#if canActivate}} - {{#unless isTemplate}} - <button id="coding-rules-quality-profile-activate" class="spacer-left">{{t 'coding_rules.activate'}}</button> - {{/unless}} - {{/if}} - - {{#if isTemplate}} - <div class="alert alert-warning"> - {{t 'coding_rules.quality_profiles.template_caption'}} - </div> - {{/if}} - - <table id="coding-rules-detail-quality-profiles" class="coding-rules-detail-quality-profiles width100"></table> -</div> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-header-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-header-view.js deleted file mode 100644 index 703d8b7830a..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-header-view.js +++ /dev/null @@ -1,72 +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 WorkspaceHeaderView from '../../components/navigator/workspace-header-view'; -import BulkChangePopup from './bulk-change-popup-view'; -import Template from './templates/coding-rules-workspace-header.hbs'; - -export default WorkspaceHeaderView.extend({ - template: Template, - - events() { - return { - ...WorkspaceHeaderView.prototype.events.apply(this, arguments), - 'click .js-back': 'onBackClick', - 'click .js-bulk-change': 'onBulkChangeClick', - 'click .js-reload': 'reload', - 'click .js-new-search': 'newSearch' - }; - }, - - onBackClick() { - this.options.app.controller.hideDetails(); - }, - - onBulkChangeClick(e) { - e.stopPropagation(); - $('body').click(); - new BulkChangePopup({ - app: this.options.app, - triggerEl: $(e.currentTarget), - bottomRight: true - }).render(); - }, - - reload(event) { - event.preventDefault(); - this.options.app.controller.fetchList(true); - }, - - newSearch() { - this.options.app.controller.newSearch(); - }, - - serializeData() { - // show "Bulk Change" button only if user has at least one QP which he administates - const canBulkChange = this.options.app.qualityProfiles.some( - profile => profile.actions && profile.actions.edit - ); - - return { - ...WorkspaceHeaderView.prototype.serializeData.apply(this, arguments), - canBulkChange - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-empty-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-empty-view.js deleted file mode 100644 index 31dfa26b8c6..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-empty-view.js +++ /dev/null @@ -1,29 +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 { translate } from '../../helpers/l10n'; - -export default Marionette.ItemView.extend({ - className: 'search-navigator-no-results', - - template() { - return translate('coding_rules.no_results'); - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-item-view.js deleted file mode 100644 index 9994a03d5b5..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-item-view.js +++ /dev/null @@ -1,145 +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 Backbone from 'backbone'; -import WorkspaceListItemView from '../../components/navigator/workspace-list-item-view'; -import ProfileActivationView from './rule/profile-activation-view'; -import RuleFilterMixin from './rule/rule-filter-mixin'; -import Template from './templates/coding-rules-workspace-list-item.hbs'; -import confirmDialog from './confirm-dialog'; -import { translate, translateWithParameters } from '../../helpers/l10n'; -import { getBaseUrl } from '../../helpers/urls'; - -export default WorkspaceListItemView.extend(RuleFilterMixin).extend({ - className: 'coding-rule', - template: Template, - - modelEvents: { - change: 'render' - }, - - events: { - click: 'selectCurrent', - dblclick: 'openRule', - 'click .js-rule': 'openRule', - 'click .js-rule-filter': 'onRuleFilterClick', - 'click .coding-rules-detail-quality-profile-activate': 'activate', - 'click .coding-rules-detail-quality-profile-change': 'change', - 'click .coding-rules-detail-quality-profile-revert': 'revert', - 'click .coding-rules-detail-quality-profile-deactivate': 'deactivate' - }, - - onRender() { - WorkspaceListItemView.prototype.onRender.apply(this, arguments); - this.$('[data-toggle="tooltip"]').tooltip({ - container: 'body' - }); - }, - - onDestroy() { - this.$('[data-toggle="tooltip"]').tooltip('destroy'); - }, - - selectCurrent() { - this.options.app.state.set({ selectedIndex: this.model.get('index') }); - }, - - openRule(event) { - const leftClick = - event.button === 0 && !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); - if (leftClick) { - event.preventDefault(); - this.$('[data-toggle="tooltip"]').tooltip('destroy'); - this.options.app.controller.showDetails(this.model); - } - }, - - activate() { - const that = this; - const selectedProfile = this.options.app.state.get('query').qprofile; - const othersQualityProfiles = this.options.app.qualityProfiles.filter( - profile => profile.key !== selectedProfile - ); - const activationView = new ProfileActivationView({ - rule: this.model, - collection: new Backbone.Collection(othersQualityProfiles), - app: this.options.app - }); - activationView.on('profileActivated', (severity, params, profile) => { - const activation = { - severity, - params, - inherit: 'NONE', - qProfile: profile - }; - that.model.set({ activation }); - }); - activationView.render(); - }, - - deactivate() { - const that = this; - const ruleKey = this.model.get('key'); - const activation = this.model.get('activation'); - confirmDialog({ - title: translate('coding_rules.deactivate'), - html: translateWithParameters('coding_rules.deactivate.confirm'), - yesHandler() { - return $.ajax({ - type: 'POST', - url: window.baseUrl + '/api/qualityprofiles/deactivate_rule', - data: { - profile_key: activation.qProfile, - rule_key: ruleKey - } - }).done(() => { - that.model.unset('activation'); - }); - } - }); - }, - - serializeData() { - const selectedProfileKey = this.options.app.state.get('query').qprofile; - const selectedProfile = - selectedProfileKey && - this.options.app.qualityProfiles.find(profile => profile.key === selectedProfileKey); - const isSelectedProfileBuiltIn = selectedProfile != null && selectedProfile.isBuiltIn; - - const canEditQualityProfile = - selectedProfile && selectedProfile.actions && selectedProfile.actions.edit; - - const permalinkPath = this.options.app.organization - ? `/organizations/${this.options.app.organization}/rules` - : '/coding_rules'; - const permalink = - getBaseUrl() + permalinkPath + '#rule_key=' + encodeURIComponent(this.model.id); - - return { - ...WorkspaceListItemView.prototype.serializeData.apply(this, arguments), - canEditQualityProfile, - tags: union(this.model.get('sysTags'), this.model.get('tags')), - selectedProfile: selectedProfileKey, - isSelectedProfileBuiltIn, - permalink - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js deleted file mode 100644 index 2baba45a4e2..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js +++ /dev/null @@ -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 key from 'keymaster'; -import WorkspaceListView from '../../components/navigator/workspace-list-view'; -import WorkspaceListItemView from './workspace-list-item-view'; -import WorkspaceListEmptyView from './workspace-list-empty-view'; -import Template from './templates/coding-rules-workspace-list.hbs'; - -export default WorkspaceListView.extend({ - template: Template, - childView: WorkspaceListItemView, - childViewContainer: '.js-list', - emptyView: WorkspaceListEmptyView, - - bindShortcuts() { - WorkspaceListView.prototype.bindShortcuts.apply(this, arguments); - const that = this; - key('right', 'list', () => { - that.options.app.controller.showDetailsForSelected(); - return false; - }); - key('a', () => { - that.options.app.controller.activateCurrent(); - return false; - }); - key('d', () => { - that.options.app.controller.deactivateCurrent(); - return false; - }); - } -}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js index 8c7eeff5622..3fd472649cd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js @@ -134,7 +134,7 @@ export default class DomainFacet extends React.PureComponent { const helper = `component_measures.domain_facets.${domain.name}.help`; const translatedHelper = translate(helper); return ( - <FacetBox> + <FacetBox property={domain.name}> <FacetHeader helper={helper !== translatedHelper ? translatedHelper : undefined} name={getLocalizedMetricDomain(domain.name)} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js index b26aae7a06e..723551831bb 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js @@ -34,7 +34,7 @@ import { translate } from '../../../helpers/l10n'; export default function ProjectOverviewFacet({ value, selected, onChange } /*: Props */) { const facetName = translate('component_measures.overview', value, 'facet'); return ( - <FacetBox> + <FacetBox property={value}> <FacetItemsList> <FacetItem active={value === selected} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap index 9c88f409c2d..864073e6026 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should display facet item list 1`] = ` -<FacetBox> +<FacetBox + property="Reliability" +> <FacetHeader name="Reliability" onClick={[Function]} @@ -129,7 +131,9 @@ exports[`should display facet item list 1`] = ` `; exports[`should display facet item list with bugs selected 1`] = ` -<FacetBox> +<FacetBox + property="Reliability" +> <FacetHeader name="Reliability" onClick={[Function]} diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index 4b9c092a275..81261fe01bf 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -24,7 +24,6 @@ import key from 'keymaster'; import { keyBy, without } from 'lodash'; import PropTypes from 'prop-types'; import PageActions from './PageActions'; -import FiltersHeader from './FiltersHeader'; import MyIssuesFilter from './MyIssuesFilter'; import Sidebar from '../sidebar/Sidebar'; import IssuesList from './IssuesList'; @@ -59,6 +58,7 @@ import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthe import { isLoggedIn } from '../../../app/types'; import ListFooter from '../../../components/controls/ListFooter'; import EmptySearch from '../../../components/common/EmptySearch'; +import FiltersHeader from '../../../components/common/FiltersHeader'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import { getBranchName, isShortLivingBranch } from '../../../helpers/branches'; import { translate, translateWithParameters } from '../../../helpers/l10n'; diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js index 6ac8ce8b88f..3d5fc68f37e 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js +++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js @@ -295,7 +295,6 @@ export default class BulkChangeModal extends React.PureComponent { <SearchSelect onSearch={this.handleAssigneeSearch} onSelect={this.handleAssigneeSelect} - minimumQueryLength={2} renderOption={this.renderAssigneeOption} resetOnBlur={false} value={this.state.assignee} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js index ea4f6e3a3db..3af7ff5ffcd 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import PageCounter from '../../../components/common/PageCounter'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; @@ -30,19 +31,13 @@ type Props = { }; */ -const IssuesCounter = (props /*: Props */) => ( - <span className={props.className}> - <strong> - {props.current != null && ( - <span> - {formatMeasure(props.current + 1, 'INT')} - {' / '} - </span> - )} - {formatMeasure(props.total, 'INT')} - </strong>{' '} - {translate('issues.issues')} - </span> -); - -export default IssuesCounter; +export default function IssuesCounter(props /*:Props*/) { + return ( + <PageCounter + className="spacer-left flash flash-heavy" + current={props.current} + label={translate('issues.issues')} + total={props.total} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.js.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.js.snap index 0af64820202..2d597c7ccae 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.js.snap @@ -1,25 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`does not show current 1`] = ` -<span> - <strong> - 987,654,321 - </strong> - - issues.issues -</span> +<PageCounter + className="spacer-left flash flash-heavy" + current={null} + label="issues.issues" + total={987654321} +/> `; exports[`formats numbers 1`] = ` -<span> - <strong> - <span> - 1,235 - / - </span> - 987,654,321 - </strong> - - issues.issues -</span> +<PageCounter + className="spacer-left flash flash-heavy" + current={1234} + label="issues.issues" + total={987654321} +/> `; diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js index ba1d3703588..f48516ee94f 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js @@ -195,7 +195,7 @@ export default class AssigneeFacet extends React.PureComponent { render() { return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js index 1953d6c474b..734a9f23699 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js @@ -95,7 +95,7 @@ export default class AuthorFacet extends React.PureComponent { render() { return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js index afa67153eb8..b3488fdb975 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js @@ -310,7 +310,7 @@ export default class CreationDateFacet extends React.PureComponent { render() { return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js index 33151732404..83926732bf2 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js @@ -111,7 +111,7 @@ export default class DirectoryFacet extends React.PureComponent { render() { const values = this.props.directories.map(dir => collapsePath(dir)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js index 1358a64bfcf..893aaa22aa9 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js @@ -46,7 +46,7 @@ export default class FacetMode extends React.PureComponent { const modes = ['count', 'effort']; return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet.mode')} /> <FacetItemsList> diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js index ec8e9d5980c..9d2587536a3 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js @@ -115,7 +115,7 @@ export default class FileFacet extends React.PureComponent { render() { const values = this.props.files.map(file => this.getFileName(file)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js index 369d97e2f4a..b7f8bf39d81 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js @@ -117,7 +117,7 @@ export default class LanguageFacet extends React.PureComponent { render() { const values = this.props.languages.map(language => this.getLanguageName(language)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js index c69d646a2cf..8a9a9a7cc8e 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js @@ -113,7 +113,7 @@ export default class ModuleFacet extends React.PureComponent { render() { const values = this.props.modules.map(module => this.getModuleName(module)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js index f61aba20e70..e93c288fbfe 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js @@ -181,7 +181,7 @@ export default class ProjectFacet extends React.PureComponent { render() { const values = this.props.projects.map(project => this.getProjectName(project)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js index 3f996e04fb4..bd59288b278 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js @@ -108,7 +108,7 @@ export default class ResolutionFacet extends React.PureComponent { const values = this.props.resolutions.map(resolution => this.getFacetItemName(resolution)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js index dd92faeaa57..daf05e65b58 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js @@ -35,6 +35,7 @@ type Props = {| languages: Array<string>, onChange: (changes: { [string]: Array<string> }) => void, onToggle: (property: string) => void, + organization: string | void; open: boolean, stats?: { [string]: number }, referencedRules: { [string]: { name: string } }, @@ -68,10 +69,11 @@ export default class RuleFacet extends React.PureComponent { }; handleSearch = (query /*: string */) => { - const { languages } = this.props; + const { languages, organization } = this.props; return searchRules({ f: 'name,langName', languages: languages.length ? languages.join() : undefined, + organization, q: query }).then(response => response.rules.map(rule => ({ label: `(${rule.langName}) ${rule.name}`, value: rule.key })) @@ -129,7 +131,7 @@ export default class RuleFacet extends React.PureComponent { render() { const values = this.props.rules.map(rule => this.getRuleName(rule)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js index 2353ea8d480..a1ec5af4071 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js @@ -92,7 +92,7 @@ export default class SeverityFacet extends React.PureComponent { const values = this.props.severities.map(severity => translate('severity', severity)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js index a512f09364e..af3f2f6a20c 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js @@ -127,6 +127,7 @@ export default class Sidebar extends React.PureComponent { languages={query.languages} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} + organization={this.props.organization && this.props.organization.key} open={!!openFacets.rules} stats={facets.rules} referencedRules={this.props.referencedRules} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js index 4bbb041ab49..d11062c7cc2 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js @@ -99,7 +99,7 @@ export default class StatusFacet extends React.PureComponent { const values = this.props.statuses.map(status => translate('issue.status', status)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js index 51465f5a61b..e02ea9f64cb 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js @@ -95,7 +95,7 @@ export default class TypeFacet extends React.PureComponent { const values = this.props.types.map(type => translate('issue.type', type)); return ( - <FacetBox> + <FacetBox property={this.property}> <FacetHeader name={translate('issues.facet', this.property)} onClear={this.handleClear} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap index d384037257b..a0afcc2cf9a 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render 1`] = ` -<FacetBox> +<FacetBox + property="assignees" +> <FacetHeader name="issues.facet.assignees" onClear={[Function]} @@ -71,7 +73,9 @@ exports[`should render footer select option 1`] = ` `; exports[`should render without stats 1`] = ` -<FacetBox> +<FacetBox + property="assignees" +> <FacetHeader name="issues.facet.assignees" onClear={[Function]} @@ -83,7 +87,9 @@ exports[`should render without stats 1`] = ` `; exports[`should select unassigned 1`] = ` -<FacetBox> +<FacetBox + property="assignees" +> <FacetHeader name="issues.facet.assignees" onClear={[Function]} @@ -145,7 +151,9 @@ exports[`should select unassigned 1`] = ` `; exports[`should select user 1`] = ` -<FacetBox> +<FacetBox + property="assignees" +> <FacetHeader name="issues.facet.assignees" onClear={[Function]} diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css index 61761cb4f37..e6ad09ee7ac 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -121,12 +121,6 @@ border: none; } -.issues-filters-header { - margin-bottom: 12px; - padding-bottom: 11px; - border-bottom: 1px solid var(--barBorderColor); -} - .issues-my-issues-filter { margin-bottom: 24px; text-align: center; @@ -138,10 +132,6 @@ text-align: right; } -.issues .search-navigator-facet-footer { - padding: 0 0 10px 0; -} - .issues .issue-list { /* no math, just a good guess */ min-width: 640px; diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.ts b/server/sonar-web/src/main/js/apps/organizations/routes.ts index 159cc9ec2eb..e6d2aa793ad 100644 --- a/server/sonar-web/src/main/js/apps/organizations/routes.ts +++ b/server/sonar-web/src/main/js/apps/organizations/routes.ts @@ -23,7 +23,6 @@ import OrganizationPageContainer from './components/OrganizationPage'; import OrganizationPageExtension from '../../app/components/extensions/OrganizationPageExtension'; import OrganizationContainer from './components/OrganizationContainer'; import OrganizationProjects from './components/OrganizationProjects'; -import OrganizationRules from './components/OrganizationRules'; import OrganizationAdminContainer from './components/OrganizationAdminContainer'; import OrganizationEdit from './components/OrganizationEdit'; import OrganizationGroups from './components/OrganizationGroups'; @@ -31,6 +30,7 @@ import OrganizationMembersContainer from './components/OrganizationMembersContai import OrganizationDelete from './components/OrganizationDelete'; import PermissionTemplateApp from '../permission-templates/components/AppContainer'; import ProjectManagementApp from '../projectsManagement/AppContainer'; +import codingRulesRoutes from '../coding-rules/routes'; import qualityGatesRoutes from '../quality-gates/routes'; import qualityProfilesRoutes from '../quality-profiles/routes'; import Issues from '../issues/components/AppContainer'; @@ -64,7 +64,8 @@ const routes = [ }, { path: 'rules', - component: OrganizationRules + component: OrganizationContainer, + childRoutes: codingRulesRoutes }, { path: 'quality_profiles', diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx index b84da6cb1f8..d05bdee1893 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx @@ -94,6 +94,7 @@ export default class Meta extends React.PureComponent<Props> { <MetaQualityProfiles component={component} customOrganizations={organizationsEnabled} + organization={component.organization} profiles={qualityProfiles} /> )} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js index 9b08058c508..c39bcf3846d 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js @@ -34,6 +34,7 @@ class MetaQualityProfiles extends React.PureComponent { component: { organization: string }, customOrganizations: boolean, languages: { [string]: { name: string } }, + organization: string | void; profiles: Array<{ key: string, language: string, name: string }> }; */ @@ -72,10 +73,11 @@ class MetaQualityProfiles extends React.PureComponent { loadDeprecatedRulesForProfile(profileKey) { const data = { - qprofile: profileKey, activation: 'true', - statuses: 'DEPRECATED', - ps: 1 + organization: this.props.organization, + ps: 1, + qprofile: profileKey, + statuses: 'DEPRECATED' }; return searchRules(data).then(r => r.total); } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx index f08c08252fe..0e22da39691 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx @@ -104,7 +104,7 @@ export default class MetaTags extends React.PureComponent<Props, State> { className="button-link" onClick={this.handleClick} ref={tagsList => (this.tagsList = tagsList)}> - <TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={true} /> + <TagsList allowUpdate={true} tags={tags.length ? tags : [translate('no_tags')]} /> </button> {popupOpen && ( <div ref={tagsSelector => (this.tagsSelector = tagsSelector)}> @@ -121,7 +121,11 @@ export default class MetaTags extends React.PureComponent<Props, State> { } else { return ( <div className="overview-meta-card overview-meta-tags"> - <TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={false} /> + <TagsList + allowUpdate={false} + className="note" + tags={tags.length ? tags : [translate('no_tags')]} + /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap index 875302462d5..f7c3da2dfca 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap @@ -108,6 +108,7 @@ exports[`should render without tags and admin rights 1`] = ` > <TagsList allowUpdate={false} + className="note" tags={ Array [ "no_tags", diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx index 44bb62dea41..2c990df3673 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx @@ -63,7 +63,7 @@ export default function ProjectCardLeak({ organization, project }: Props) { {isPrivate && ( <PrivateBadge className="spacer-left" qualifier="TRK" tooltipPlacement="left" /> )} - {hasTags && <TagsList tags={project.tags} customClass="spacer-left" />} + {hasTags && <TagsList className="spacer-left note" tags={project.tags} />} </div> {project.analysisDate && project.leakPeriodDate && ( diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx index 104be36dca3..275aa8fb49a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx @@ -62,7 +62,7 @@ export default function ProjectCardOverall({ organization, project }: Props) { {isPrivate && ( <PrivateBadge className="spacer-left" qualifier="TRK" tooltipPlacement="left" /> )} - {hasTags && <TagsList tags={project.tags} customClass="spacer-left" />} + {hasTags && <TagsList className="spacer-left note" tags={project.tags} />} </div> {project.analysisDate && ( <div className="project-card-dates note text-right"> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx index 1983078ec2d..1c9a7da96c9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx @@ -68,7 +68,7 @@ it('should render action', () => { it('should render rule', () => { const events = [createEvent()]; const changelog = shallow(<Changelog events={events} organization={null} />); - expect(changelog.find('Link').prop('to')).toContain('rule_key=squid1234'); + expect(changelog.find('Link').prop('to')).toHaveProperty('query', { rule_key: 'squid1234' }); }); it('should render ChangesList', () => { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx index 76136ad67d6..4acaf357bd9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx @@ -73,7 +73,7 @@ it('should compare', () => { const leftDiffs = output.find('.js-comparison-in-left'); expect(leftDiffs.length).toBe(1); expect(leftDiffs.find(Link).length).toBe(1); - expect(leftDiffs.find(Link).prop('to')).toContain('rule_key=rule1'); + expect(leftDiffs.find(Link).prop('to')).toHaveProperty('query', { rule_key: 'rule1' }); expect(leftDiffs.find(Link).prop('children')).toContain('rule1'); expect(leftDiffs.find(SeverityIcon).length).toBe(1); expect(leftDiffs.find(SeverityIcon).prop('severity')).toBe('BLOCKER'); @@ -86,7 +86,7 @@ it('should compare', () => { .at(0) .find(Link) .prop('to') - ).toContain('rule_key=rule2'); + ).toHaveProperty('query', { rule_key: 'rule2' }); expect( rightDiffs .at(0) @@ -108,7 +108,7 @@ it('should compare', () => { .find(Link) .at(0) .prop('to') - ).toContain('rule_key=rule4'); + ).toHaveProperty('query', { rule_key: 'rule4' }); expect( modifiedDiffs .find(Link) diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index 7e53929904b..01f38890790 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -4,7 +4,15 @@ exports[`renders with all permissions 1`] = ` <ActionsDropdown> <ActionsDropdownItem id="quality-profile-activate-more-rules" - to="/organizations/org/rules#qprofile=foo|activation=false" + to={ + Object { + "pathname": "/organizations/org/rules", + "query": Object { + "activation": "false", + "qprofile": "foo", + }, + } + } > quality_profiles.activate_more_rules </ActionsDropdownItem> @@ -88,7 +96,15 @@ exports[`renders with permission to edit only 1`] = ` <ActionsDropdown> <ActionsDropdownItem id="quality-profile-activate-more-rules" - to="/organizations/org/rules#qprofile=foo|activation=false" + to={ + Object { + "pathname": "/organizations/org/rules", + "query": Object { + "activation": "false", + "qprofile": "foo", + }, + } + } > quality_profiles.activate_more_rules </ActionsDropdownItem> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx index c7a20068385..93bad2515b0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx @@ -91,17 +91,19 @@ export default class ProfileRules extends React.PureComponent<Props, State> { loadAllRules() { return searchRules({ languages: this.props.profile.language, - ps: 1, - facets: 'types' + facets: 'types', + organization: this.props.organization || undefined, + ps: 1 }); } loadActivatedRules() { return searchRules({ - qprofile: this.props.profile.key, activation: 'true', + facets: 'types', + organization: this.props.organization || undefined, ps: 1, - facets: 'types' + qprofile: this.props.profile.key }); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx index 5ea85d8a669..e2cd08698ce 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx @@ -30,7 +30,6 @@ interface Props { } export default function ProfileRulesDeprecatedWarning(props: Props) { - const url = getDeprecatedActiveRulesUrl({ qprofile: props.profile }, props.organization); return ( <div className="quality-profile-rules-deprecated clearfix"> <span className="pull-left"> @@ -39,7 +38,9 @@ export default function ProfileRulesDeprecatedWarning(props: Props) { <i className="icon-help spacer-left" /> </Tooltip> </span> - <Link className="pull-right" to={url}> + <Link + className="pull-right" + to={getDeprecatedActiveRulesUrl({ qprofile: props.profile }, props.organization)}> {props.activeDeprecatedRules} </Link> </div> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap index c366398e56e..2fd0450b1bd 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap @@ -74,7 +74,15 @@ exports[`should show a button to activate more rules for admins 1`] = ` className="button js-activate-rules" onlyActiveOnIndex={false} style={Object {}} - to="/organizations/foo/rules#qprofile=foo|activation=false" + to={ + Object { + "pathname": "/organizations/foo/rules", + "query": Object { + "activation": "false", + "qprofile": "foo", + }, + } + } > quality_profiles.activate_more </Link> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap index 54fa91cc49d..4072c84db21 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap @@ -21,7 +21,16 @@ exports[`should render correctly 1`] = ` className="pull-right" onlyActiveOnIndex={false} style={Object {}} - to="/organizations/foo/rules#qprofile=bar|activation=true|statuses=DEPRECATED" + to={ + Object { + "pathname": "/organizations/foo/rules", + "query": Object { + "activation": "true", + "qprofile": "bar", + "statuses": "DEPRECATED", + }, + } + } > 18 </Link> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap index 62866095c17..1a879d91adf 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap @@ -17,7 +17,16 @@ exports[`should render correctly 1`] = ` <Link onlyActiveOnIndex={false} style={Object {}} - to="/organizations/foo/rules#qprofile=bar|activation=true|types=BUG" + to={ + Object { + "pathname": "/organizations/foo/rules", + "query": Object { + "activation": "true", + "qprofile": "bar", + "types": "BUG", + }, + } + } > 3 </Link> @@ -29,7 +38,16 @@ exports[`should render correctly 1`] = ` className="small text-muted" onlyActiveOnIndex={false} style={Object {}} - to="/organizations/foo/rules#qprofile=bar|activation=false|types=BUG" + to={ + Object { + "pathname": "/organizations/foo/rules", + "query": Object { + "activation": "false", + "qprofile": "bar", + "types": "BUG", + }, + } + } > 7 </Link> @@ -54,7 +72,16 @@ exports[`should render correctly if there is 0 rules 1`] = ` <Link onlyActiveOnIndex={false} style={Object {}} - to="/coding_rules#qprofile=bar|activation=true|types=VULNERABILITY" + to={ + Object { + "pathname": "/coding_rules", + "query": Object { + "activation": "true", + "qprofile": "bar", + "types": "VULNERABILITY", + }, + } + } > 0 </Link> @@ -88,7 +115,16 @@ exports[`should render correctly if there is missing data 1`] = ` <Link onlyActiveOnIndex={false} style={Object {}} - to="/coding_rules#qprofile=bar|activation=true|types=VULNERABILITY" + to={ + Object { + "pathname": "/coding_rules", + "query": Object { + "activation": "true", + "qprofile": "bar", + "types": "VULNERABILITY", + }, + } + } > 5 </Link> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap index f8686659326..05ad9a31580 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap @@ -13,7 +13,15 @@ exports[`should render correctly 1`] = ` <Link onlyActiveOnIndex={false} style={Object {}} - to="/organizations/foo/rules#qprofile=bar|activation=true" + to={ + Object { + "pathname": "/organizations/foo/rules", + "query": Object { + "activation": "true", + "qprofile": "bar", + }, + } + } > <strong> 3 @@ -27,7 +35,15 @@ exports[`should render correctly 1`] = ` className="small text-muted" onlyActiveOnIndex={false} style={Object {}} - to="/organizations/foo/rules#qprofile=bar|activation=false" + to={ + Object { + "pathname": "/organizations/foo/rules", + "query": Object { + "activation": "false", + "qprofile": "bar", + }, + } + } > <strong> 7 @@ -50,7 +66,15 @@ exports[`should render correctly if there is 0 rules 1`] = ` <Link onlyActiveOnIndex={false} style={Object {}} - to="/coding_rules#qprofile=bar|activation=true" + to={ + Object { + "pathname": "/coding_rules", + "query": Object { + "activation": "true", + "qprofile": "bar", + }, + } + } > <strong> 0 @@ -82,7 +106,15 @@ exports[`should render correctly if there is missing data 1`] = ` <Link onlyActiveOnIndex={false} style={Object {}} - to="/coding_rules#qprofile=bar|activation=true" + to={ + Object { + "pathname": "/coding_rules", + "query": Object { + "activation": "true", + "qprofile": "bar", + }, + } + } > <strong> 5 diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap index 481846b23ad..7edb44e1c23 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap @@ -21,7 +21,17 @@ exports[`should render correctly 1`] = ` className="pull-right" onlyActiveOnIndex={false} style={Object {}} - to="/organizations/foo/rules#qprofile=bar|activation=false|compareToProfile=baz|languages=Java" + to={ + Object { + "pathname": "/organizations/foo/rules", + "query": Object { + "activation": "false", + "compareToProfile": "baz", + "languages": "Java", + "qprofile": "bar", + }, + } + } > 158 </Link> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx index af4f60216e0..d4b1df5a548 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx @@ -75,11 +75,12 @@ export default class EvolutionRules extends React.PureComponent<Props, State> { loadLatestRules() { const data = { - available_since: this.periodStartDate, - s: 'createdAt', asc: false, + available_since: this.periodStartDate, + f: 'name,langName,actives', + organization: this.props.organization || undefined, ps: RULES_LIMIT, - f: 'name,langName,actives' + s: 'createdAt' }; searchRules(data).then( diff --git a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx index 18b7dbd6f95..38ebd834989 100644 --- a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx +++ b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx @@ -21,8 +21,9 @@ import * as React from 'react'; import * as classNames from 'classnames'; export interface BubblePopupPosition { - top: number; - right: number; + top?: number; + left?: number; + right?: number; } interface Props { diff --git a/server/sonar-web/src/main/js/components/common/DeferredSpinner.tsx b/server/sonar-web/src/main/js/components/common/DeferredSpinner.tsx index dee23f8152b..ffc8b0668b7 100644 --- a/server/sonar-web/src/main/js/components/common/DeferredSpinner.tsx +++ b/server/sonar-web/src/main/js/components/common/DeferredSpinner.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import * as classNames from 'classnames'; interface Props { - children?: JSX.Element | JSX.Element[]; + children?: React.ReactNode; className?: string; loading?: boolean; customSpinner?: JSX.Element; diff --git a/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js b/server/sonar-web/src/main/js/components/common/FiltersHeader.tsx index c0f0e8b0998..33567469928 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js +++ b/server/sonar-web/src/main/js/components/common/FiltersHeader.tsx @@ -17,32 +17,30 @@ * 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 { translate } from '../../../helpers/l10n'; +import * as React from 'react'; +import { translate } from '../../helpers/l10n'; -/*:: -type Props = { - displayReset: boolean, - onReset: () => void -}; -*/ - -export default class FiltersHeader extends React.PureComponent { - /*:: props: Props; */ +interface Props { + displayReset: boolean; + onReset: () => void; +} - handleResetClick = (e /*: Event & { currentTarget: HTMLElement } */) => { - e.preventDefault(); - e.currentTarget.blur(); +export default class FiltersHeader extends React.PureComponent<Props> { + handleResetClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault(); + event.currentTarget.blur(); this.props.onReset(); }; render() { return ( - <div className="issues-filters-header"> + <div className="search-navigator-filters-header"> {this.props.displayReset && ( <div className="pull-right"> - <button className="button-red" onClick={this.handleResetClick}> + <button + className="button-red" + id="coding-rules-clear-all-filters" + onClick={this.handleResetClick}> {translate('clear_all_filters')} </button> </div> diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.js b/server/sonar-web/src/main/js/components/common/MarkdownTips.tsx index aeb95c07d36..93755541652 100644 --- a/server/sonar-web/src/main/js/components/common/MarkdownTips.js +++ b/server/sonar-web/src/main/js/components/common/MarkdownTips.tsx @@ -17,13 +17,12 @@ * 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 * as React from 'react'; import { getMarkdownHelpUrl } from '../../helpers/urls'; import { translate } from '../../helpers/l10n'; export default class MarkdownTips extends React.PureComponent { - handleClick(evt /*: MouseEvent */) { + handleClick(evt: React.SyntheticEvent<HTMLAnchorElement>) { evt.preventDefault(); window.open(getMarkdownHelpUrl(), 'Markdown', 'height=300,width=600,scrollbars=1,resizable=1'); } diff --git a/server/sonar-web/src/main/js/components/common/PageCounter.tsx b/server/sonar-web/src/main/js/components/common/PageCounter.tsx new file mode 100644 index 00000000000..2ef4e27df67 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/PageCounter.tsx @@ -0,0 +1,41 @@ +/* + * 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 { formatMeasure } from '../../helpers/measures'; + +interface Props { + className?: string; + current?: number; + label: string; + total: number; +} + +export default function PageCounter({ className, current, label, total }: Props) { + return ( + <div className={classNames('display-inline-block', className)}> + <strong className="little-spacer-right"> + {current !== undefined && formatMeasure(current + 1, 'INT') + ' / '} + <span className="js-page-counter-total">{formatMeasure(total, 'INT')}</span> + </strong> + {label} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/components/common/action-options-view.js b/server/sonar-web/src/main/js/components/common/action-options-view.js deleted file mode 100644 index 8e65eef4c5a..00000000000 --- a/server/sonar-web/src/main/js/components/common/action-options-view.js +++ /dev/null @@ -1,133 +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 key from 'keymaster'; -import PopupView from './popup'; - -export default PopupView.extend({ - className: 'bubble-popup bubble-popup-menu', - keyScope: 'action-options', - - ui: { - options: '.menu > li > a' - }, - - events() { - return { - 'click @ui.options': 'selectOption', - 'mouseenter @ui.options': 'activateOptionByPointer' - }; - }, - - initialize() { - this.bindShortcuts(); - }, - - onRender() { - PopupView.prototype.onRender.apply(this, arguments); - this.selectInitialOption(); - }, - - getOptions() { - return this.$('.menu > li > a'); - }, - - getActiveOption() { - return this.getOptions().filter('.active'); - }, - - makeActive(option) { - if (option.length > 0) { - this.getOptions() - .removeClass('active') - .tooltip('hide'); - option.addClass('active').tooltip('show'); - } - }, - - selectInitialOption() { - this.makeActive(this.getOptions().first()); - }, - - selectNextOption() { - this.makeActive( - this.getActiveOption() - .parent() - .nextAll('li:not(.divider)') - .first() - .children('a') - ); - return false; - }, - - selectPreviousOption() { - this.makeActive( - this.getActiveOption() - .parent() - .prevAll('li:not(.divider)') - .first() - .children('a') - ); - return false; - }, - - activateOptionByPointer(e) { - this.makeActive($(e.currentTarget)); - }, - - bindShortcuts() { - const that = this; - this.currentKeyScope = key.getScope(); - key.setScope(this.keyScope); - key('down', this.keyScope, () => that.selectNextOption()); - key('up', this.keyScope, () => that.selectPreviousOption()); - key('return', this.keyScope, () => that.selectActiveOption()); - key('escape', this.keyScope, () => that.destroy()); - key('backspace', this.keyScope, () => false); - key('shift+tab', this.keyScope, () => false); - }, - - unbindShortcuts() { - key.unbind('down', this.keyScope); - key.unbind('up', this.keyScope); - key.unbind('return', this.keyScope); - key.unbind('escape', this.keyScope); - key.unbind('backspace', this.keyScope); - key.unbind('tab', this.keyScope); - key.unbind('shift+tab', this.keyScope); - key.setScope(this.currentKeyScope); - }, - - onDestroy() { - PopupView.prototype.onDestroy.apply(this, arguments); - this.unbindShortcuts(); - this.$('[data-toggle="tooltip"]').tooltip('destroy'); - $('.tooltip').remove(); - }, - - selectOption(e) { - e.preventDefault(); - this.destroy(); - }, - - selectActiveOption() { - this.getActiveOption().click(); - } -}); diff --git a/server/sonar-web/src/main/js/components/common/templates/_markdown-tips.hbs b/server/sonar-web/src/main/js/components/common/templates/_markdown-tips.hbs deleted file mode 100644 index d6e538797c3..00000000000 --- a/server/sonar-web/src/main/js/components/common/templates/_markdown-tips.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="markdown-tips"> - <a href="#" onclick="window.open(window.baseUrl + '/markdown/help','markdown','height=300,width=600,scrollbars=1,resizable=1');return false;">{{t 'markdown.helplink'}}</a> : - *{{t 'bold'}}* ``{{t 'code'}}`` * {{t 'bulleted_point'}} -</div> diff --git a/server/sonar-web/src/main/js/components/controls/ReloadButton.tsx b/server/sonar-web/src/main/js/components/controls/ReloadButton.tsx new file mode 100644 index 00000000000..134e61adc7e --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/ReloadButton.tsx @@ -0,0 +1,59 @@ +/* + * 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 Tooltip from './Tooltip'; +import * as theme from '../../app/theme'; +import { translate } from '../../helpers/l10n'; + +interface Props { + className?: string; + onClick: () => void; +} + +export default class ReloadButton extends React.PureComponent<Props> { + handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onClick(); + }; + + renderIcon = () => ( + <svg width="18" height="24" viewBox="0 0 18 24"> + <path + fill={theme.secondFontColor} + 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> + ); + + render() { + return ( + <Tooltip overlay={translate('reload')}> + <a + className={classNames('link-no-underline', this.props.className)} + href="#" + onClick={this.handleClick}> + {this.renderIcon()} + </a> + </Tooltip> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/SearchBox.tsx b/server/sonar-web/src/main/js/components/controls/SearchBox.tsx index d85afa7fb5e..a4bb8196492 100644 --- a/server/sonar-web/src/main/js/components/controls/SearchBox.tsx +++ b/server/sonar-web/src/main/js/components/controls/SearchBox.tsx @@ -29,7 +29,9 @@ import './SearchBox.css'; interface Props { autoFocus?: boolean; + className?: string; innerRef?: (node: HTMLInputElement | null) => void; + id?: string; minLength?: number; onChange: (value: string) => void; onClick?: React.MouseEventHandler<HTMLInputElement>; @@ -127,7 +129,7 @@ export default class SearchBox extends React.PureComponent<Props, State> { const tooShort = minLength !== undefined && value.length > 0 && value.length < minLength; return ( - <div className="search-box"> + <div className={classNames('search-box', this.props.className)} id={this.props.id}> <input autoComplete="off" autoFocus={this.props.autoFocus} diff --git a/server/sonar-web/src/main/js/components/controls/SearchSelect.js b/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx index bed2a3acd1b..88b6d07ed5b 100644 --- a/server/sonar-web/src/main/js/components/controls/SearchSelect.js +++ b/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx @@ -17,48 +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. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { debounce } from 'lodash'; import Select from '../../components/controls/Select'; import { translate, translateWithParameters } from '../../helpers/l10n'; -/*:: -type Option = { label: string, value: string }; -*/ +type Option = { label: string; value: string }; -/*:: -type Props = {| - autofocus: boolean, - minimumQueryLength: number, - onSearch: (query: string) => Promise<Array<Option>>, - onSelect: (value: string) => void, - renderOption?: (option: Object) => React.Element<*>, - resetOnBlur: boolean, - value?: string -|}; -*/ +interface Props { + autofocus?: boolean; + minimumQueryLength?: number; + onSearch: (query: string) => Promise<Option[]>; + onSelect: (value: string) => void; + renderOption?: (option: Object) => JSX.Element; + resetOnBlur?: boolean; + value?: string; +} -/*:: -type State = { - loading: boolean, - options: Array<Option>, - query: string -}; -*/ +interface State { + loading: boolean; + options: Option[]; + query: string; +} -export default class SearchSelect extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: props: Props; */ - /*:: state: State; */ +export default class SearchSelect extends React.PureComponent<Props, State> { + mounted: boolean; static defaultProps = { autofocus: true, - minimumQueryLength: 2, resetOnBlur: true }; - constructor(props /*: Props */) { + constructor(props: Props) { super(props); this.state = { loading: false, options: [], query: '' }; this.search = debounce(this.search, 250); @@ -72,7 +62,11 @@ export default class SearchSelect extends React.PureComponent { this.mounted = false; } - search = (query /*: string */) => { + get minimumQueryLength() { + return this.props.minimumQueryLength || 2; + } + + search = (query: string) => { this.props.onSearch(query).then( options => { if (this.mounted) { @@ -87,14 +81,14 @@ export default class SearchSelect extends React.PureComponent { ); }; - handleChange = (option /*: Option */) => { + handleChange = (option: Option) => { this.props.onSelect(option.value); }; - handleInputChange = (query /*: string */) => { + handleInputChange = (query: string) => { // `onInputChange` is called with an empty string after a user selects a value // in this case we shouldn't reset `options`, because it also resets select value :( - if (query.length >= this.props.minimumQueryLength) { + if (query.length >= this.minimumQueryLength) { this.setState({ loading: true, query }); this.search(query); } else if (query.length > 0) { @@ -109,20 +103,18 @@ export default class SearchSelect extends React.PureComponent { return ( <Select autofocus={this.props.autofocus} - cache={false} className="input-super-large" clearable={false} filterOption={this.handleFilterOption} isLoading={this.state.loading} noResultsText={ - this.state.query.length < this.props.minimumQueryLength - ? translateWithParameters('select2.tooShort', this.props.minimumQueryLength) + this.state.query.length < this.minimumQueryLength + ? translateWithParameters('select2.tooShort', this.minimumQueryLength) : translate('select2.noMatches') } onBlurResetsInput={this.props.resetOnBlur} onChange={this.handleChange} onInputChange={this.handleInputChange} - onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined} optionRenderer={this.props.renderOption} options={this.state.options} placeholder={translate('search_verb')} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap index 00b34224f8c..0ecd070f31d 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap @@ -3,7 +3,6 @@ exports[`should render Select 1`] = ` <Select autofocus={true} - cache={false} className="input-super-large" clearable={false} filterOption={[Function]} diff --git a/server/sonar-web/src/main/js/components/controls/react-select.css b/server/sonar-web/src/main/js/components/controls/react-select.css index d7d01293472..bc84ab6c512 100644 --- a/server/sonar-web/src/main/js/components/controls/react-select.css +++ b/server/sonar-web/src/main/js/components/controls/react-select.css @@ -99,11 +99,11 @@ white-space: nowrap; } -.Select-value svg, .Select-value [class^='icon-'] { padding-top: 5px; } +.Select-value svg, .Select-value img { padding-top: 3px; } @@ -391,10 +391,15 @@ } .Select--multi .Select-value-label { + display: inline-block; + max-width: 200px; border-bottom-right-radius: 2px; border-top-right-radius: 2px; cursor: default; padding: 2px 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .Select--multi a.Select-value-label { diff --git a/server/sonar-web/src/main/js/components/facet/FacetBox.js b/server/sonar-web/src/main/js/components/facet/FacetBox.tsx index dcc8268b65e..1db88a9a6d2 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetBox.js +++ b/server/sonar-web/src/main/js/components/facet/FacetBox.tsx @@ -17,15 +17,21 @@ * 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 * as React from 'react'; +import * as classNames from 'classnames'; -/*:: -type Props = {| - children?: React.Element<*> -|}; -*/ +interface Props { + className?: string; + children: React.ReactNode; + property: string; +} -export default function FacetBox(props /*: Props */) { - return <div className="search-navigator-facet-box">{props.children}</div>; +export default function FacetBox(props: Props) { + return ( + <div + className={classNames('search-navigator-facet-box', props.className)} + data-property={props.property}> + {props.children} + </div> + ); } diff --git a/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js b/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx index f67f8a8c010..2f34ccf9fe0 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js +++ b/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx @@ -17,18 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const React = require('react'); -const { renderToString } = require('react-dom/server'); -const Handlebars = require('handlebars/runtime'); -const WithStore = require('../../components/shared/WithStore').default; -const Avatar = require('../../components/ui/Avatar').default; +import * as React from 'react'; +import SearchSelect from '../controls/SearchSelect'; -module.exports = function(hash, name, size) { - return new Handlebars.default.SafeString( - renderToString( - <WithStore> - <Avatar hash={hash} name={name} size={size} /> - </WithStore> - ) +type Option = { label: string; value: string }; + +interface Props { + minimumQueryLength?: number; + onSearch: (query: string) => Promise<Option[]>; + onSelect: (value: string) => void; + renderOption?: (option: Object) => JSX.Element; +} + +export default function FacetFooter(props: Props) { + return ( + <div className="search-navigator-facet-footer"> + <SearchSelect autofocus={false} {...props} /> + </div> ); -}; +} diff --git a/server/sonar-web/src/main/js/components/facet/FacetHeader.js b/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx index 8bda5f29d4b..cfe3f068bc4 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetHeader.js +++ b/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx @@ -17,32 +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 * as React from 'react'; import OpenCloseIcon from '../icons-components/OpenCloseIcon'; import HelpIcon from '../icons-components/HelpIcon'; import Tooltip from '../controls/Tooltip'; import { translate, translateWithParameters } from '../../helpers/l10n'; -/*:: -type Props = {| - helper?: string, - name: string, - onClear?: () => void, - onClick?: () => void, - open: boolean, - values?: Array<string> -|}; -*/ - -export default class FacetHeader extends React.PureComponent { - /*:: props: Props; */ - - static defaultProps = { - open: true - }; +interface Props { + helper?: string; + name: string; + onClear?: () => void; + onClick?: () => void; + open: boolean; + values?: string[]; +} - handleClearClick = (event /*: Event & { currentTarget: HTMLElement } */) => { +export default class FacetHeader extends React.PureComponent<Props> { + handleClearClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { event.preventDefault(); event.currentTarget.blur(); if (this.props.onClear) { @@ -50,7 +41,7 @@ export default class FacetHeader extends React.PureComponent { } }; - handleClick = (event /*: Event & { currentTarget: HTMLElement } */) => { + handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); if (this.props.onClick) { diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.js b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx index e4d97da0cd7..a960cb8af0e 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetItem.js +++ b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx @@ -17,48 +17,45 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; -/*:: -type Props = {| - active: boolean, - disabled: boolean, - halfWidth: boolean, - name: string | React.Element<*>, - onClick: string => void, - stat?: ?(string | React.Element<*>), - value: string -|}; -*/ - -export default class FacetItem extends React.PureComponent { - /*:: props: Props; */ +export interface Props { + active?: boolean; + className?: string; + disabled?: boolean; + halfWidth?: boolean; + name: React.ReactNode; + onClick: (x: string) => void; + stat?: React.ReactNode; + value: string; +} +export default class FacetItem extends React.PureComponent<Props> { static defaultProps = { disabled: false, halfWidth: false }; - handleClick = (event /*: Event & { currentTarget: HTMLElement } */) => { + handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); + event.currentTarget.blur(); this.props.onClick(this.props.value); }; render() { - const className = classNames('facet', 'search-navigator-facet', { + const className = classNames('facet', 'search-navigator-facet', this.props.className, { active: this.props.active, 'search-navigator-facet-half': this.props.halfWidth }); return this.props.disabled ? ( - <span className={className}> + <span className={className} data-facet={this.props.value}> <span className="facet-name">{this.props.name}</span> {this.props.stat != null && <span className="facet-stat">{this.props.stat}</span>} </span> ) : ( - <a className={className} href="#" onClick={this.handleClick}> + <a className={className} data-facet={this.props.value} href="#" onClick={this.handleClick}> <span className="facet-name">{this.props.name}</span> {this.props.stat != null && <span className="facet-stat">{this.props.stat}</span>} </a> diff --git a/server/sonar-web/src/main/js/components/facet/FacetItemsList.js b/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx index da8d2c8b303..3fa2bf5e86e 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetItemsList.js +++ b/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx @@ -17,15 +17,12 @@ * 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 * as React from 'react'; -/*:: -type Props = {| - children?: Array<React.Element<*>> -|}; -*/ +interface Props { + children?: React.ReactNode; +} -export default function FacetItemsList(props /*: Props */) { +export default function FacetItemsList(props: Props) { return <div className="search-navigator-facet-list">{props.children}</div>; } diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.tsx index 005ecce2108..3595b342625 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.js +++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.tsx @@ -17,15 +17,14 @@ * 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 * as React from 'react'; import { shallow } from 'enzyme'; import FacetBox from '../FacetBox'; it('should render', () => { expect( shallow( - <FacetBox> + <FacetBox property="foo"> <div /> </FacetBox> ) diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.tsx index c287824ece9..ce1f6f4f43c 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.js +++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.tsx @@ -17,8 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { shallow } from 'enzyme'; import FacetFooter from '../FacetFooter'; diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.tsx index 7249c7cff4f..3a292e5f84b 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.js +++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.tsx @@ -17,11 +17,10 @@ * 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 * as React from 'react'; import { shallow } from 'enzyme'; -import { click } from '../../../helpers/testUtils'; import FacetHeader from '../FacetHeader'; +import { click } from '../../../helpers/testUtils'; it('should render open facet with value', () => { expect( diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx index b3cd9936b4e..e16c5020383 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.js +++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx @@ -17,16 +17,10 @@ * 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 * as React from 'react'; import { shallow } from 'enzyme'; import { click } from '../../../helpers/testUtils'; -import FacetItem from '../FacetItem'; - -const renderFacetItem = (props /*: {} */) => - shallow( - <FacetItem active={false} name="foo" onClick={jest.fn()} stat={null} value="bar" {...props} /> - ); +import FacetItem, { Props } from '../FacetItem'; it('should render active', () => { expect(renderFacetItem({ active: true })).toMatchSnapshot(); @@ -48,13 +42,15 @@ it('should render half width', () => { expect(renderFacetItem({ halfWidth: true })).toMatchSnapshot(); }); -it('should render effort stat', () => { - expect(renderFacetItem({ facetMode: 'effort', stat: '1234' })).toMatchSnapshot(); -}); - it('should call onClick', () => { const onClick = jest.fn(); const wrapper = renderFacetItem({ onClick }); - click(wrapper, { currentTarget: { dataset: { value: 'bar' } } }); + click(wrapper, { currentTarget: { blur() {}, dataset: { value: 'bar' } } }); expect(onClick).toHaveBeenCalled(); }); + +function renderFacetItem(props?: Partial<Props>) { + return shallow( + <FacetItem active={false} name="foo" onClick={jest.fn()} stat={null} value="bar" {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.js b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx index ad6b3a53540..5bd2034349c 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.js +++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx @@ -17,8 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { shallow } from 'enzyme'; import FacetItemsList from '../FacetItemsList'; diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.tsx.snap index e28d4538d46..317329065b1 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.js.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.tsx.snap @@ -3,6 +3,7 @@ exports[`should render 1`] = ` <div className="search-navigator-facet-box" + data-property="foo" > <div /> </div> diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.tsx.snap index e57d0a002f6..3fab99c21a5 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.tsx.snap @@ -6,7 +6,6 @@ exports[`should render 1`] = ` > <SearchSelect autofocus={false} - minimumQueryLength={2} onSearch={[MockFunction]} onSelect={[MockFunction]} resetOnBlur={true} diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.tsx.snap index dad6166c959..dad6166c959 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.js.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.tsx.snap diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap index 6aff532c59f..c0cf80746aa 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.js.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap @@ -3,6 +3,7 @@ exports[`should render active 1`] = ` <a className="facet search-navigator-facet active" + data-facet="bar" href="#" onClick={[Function]} > @@ -17,6 +18,7 @@ exports[`should render active 1`] = ` exports[`should render disabled 1`] = ` <span className="facet search-navigator-facet" + data-facet="bar" > <span className="facet-name" @@ -26,28 +28,10 @@ exports[`should render disabled 1`] = ` </span> `; -exports[`should render effort stat 1`] = ` -<a - className="facet search-navigator-facet" - href="#" - onClick={[Function]} -> - <span - className="facet-name" - > - foo - </span> - <span - className="facet-stat" - > - 1234 - </span> -</a> -`; - exports[`should render half width 1`] = ` <a className="facet search-navigator-facet search-navigator-facet-half" + data-facet="bar" href="#" onClick={[Function]} > @@ -62,6 +46,7 @@ exports[`should render half width 1`] = ` exports[`should render inactive 1`] = ` <a className="facet search-navigator-facet" + data-facet="bar" href="#" onClick={[Function]} > @@ -76,6 +61,7 @@ exports[`should render inactive 1`] = ` exports[`should render stat 1`] = ` <a className="facet search-navigator-facet" + data-facet="bar" href="#" onClick={[Function]} > diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.js.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap index 9962cfc364e..9962cfc364e 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.js.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js index ee6a6e08e25..e4ba9ab9968 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js @@ -78,8 +78,8 @@ export default class IssueTags extends React.PureComponent { className={'js-issue-edit-tags button-link issue-action issue-action-with-options'} onClick={this.toggleSetTags}> <TagsList - tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]} allowUpdate={this.props.canSetTags} + tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]} /> </button> </BubblePopupHelper> @@ -87,8 +87,9 @@ export default class IssueTags extends React.PureComponent { } else { return ( <TagsList - tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]} allowUpdate={this.props.canSetTags} + className="note" + tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]} /> ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap index 055531ca38d..178f8b75e19 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap @@ -93,6 +93,7 @@ exports[`should render with the action 1`] = ` exports[`should render without the action when the correct rights are missing 1`] = ` <TagsList allowUpdate={false} + className="note" tags={ Array [ "issue.no_tag", diff --git a/server/sonar-web/src/main/js/components/navigator/controller.js b/server/sonar-web/src/main/js/components/navigator/controller.js deleted file mode 100644 index 6b3f00caa74..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/controller.js +++ /dev/null @@ -1,149 +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 { uniq } from 'lodash'; -import Marionette from 'backbone.marionette'; - -export default Marionette.Controller.extend({ - pageSize: 50, - - initialize(options) { - this.app = options.app; - this.listenTo(options.app.state, 'change:query', this.fetchList); - }, - - _allFacets() { - return this.options.app.state.get('allFacets').map(facet => { - return { property: facet }; - }); - }, - - _enabledFacets() { - const that = this; - let facets = this.options.app.state.get('facets'); - const criteria = Object.keys(this.options.app.state.get('query')); - facets = facets.concat(criteria); - facets = facets.map(facet => { - return that.options.app.state.get('transform')[facet] != null - ? that.options.app.state.get('transform')[facet] - : facet; - }); - facets = uniq(facets); - return facets.filter(facet => that.options.app.state.get('allFacets').indexOf(facet) !== -1); - }, - - _facetsFromServer() { - const that = this; - const facets = this._enabledFacets(); - return facets.filter( - facet => that.options.app.state.get('facetsFromServer').indexOf(facet) !== -1 - ); - }, - - fetchList() {}, - - fetchNextPage() { - this.options.app.state.nextPage(); - return this.fetchList(false); - }, - - enableFacet(id) { - const facet = this.options.app.facets.get(id); - if (facet.has('values') || this.options.app.state.get('facetsFromServer').indexOf(id) === -1) { - facet.set({ enabled: true }); - } else { - this.requestFacet(id).then(() => { - facet.set({ enabled: true }); - }); - } - }, - - disableFacet(id) { - const facet = this.options.app.facets.get(id); - facet.set({ enabled: false }); - this.options.app.facetsView.children.findByModel(facet).disable(); - }, - - toggleFacet(id) { - const facet = this.options.app.facets.get(id); - if (facet.get('enabled')) { - this.disableFacet(id); - } else { - this.enableFacet(id); - } - }, - - enableFacets(facets) { - facets.forEach(this.enableFacet, this); - }, - - newSearch() { - this.options.app.state.setQuery({}); - }, - - parseQuery(query, separator) { - separator = separator || '|'; - const q = {}; - (query || '').split(separator).forEach(t => { - const tokens = t.split('='); - if (tokens[0] && tokens[1] != null) { - q[tokens[0]] = decodeURIComponent(tokens[1]); - } - }); - return q; - }, - - getQuery(separator) { - separator = separator || '|'; - const filter = this.options.app.state.get('query'); - const route = []; - Object.keys(filter).forEach(property => { - route.push(`${property}=${encodeURIComponent(filter[property])}`); - }); - return route.join(separator); - }, - - getRoute(separator) { - separator = separator || '|'; - return this.getQuery(separator); - }, - - selectNext() { - const index = this.options.app.state.get('selectedIndex') + 1; - if (index < this.options.app.list.length) { - this.options.app.state.set({ selectedIndex: index }); - } else if (!this.options.app.state.get('maxResultsReached')) { - const that = this; - this.fetchNextPage().then(() => { - that.options.app.state.set({ selectedIndex: index }); - }); - } else { - this.options.app.list.trigger('limitReached'); - } - }, - - selectPrev() { - const index = this.options.app.state.get('selectedIndex') - 1; - if (index >= 0) { - this.options.app.state.set({ selectedIndex: index }); - } else { - this.options.app.list.trigger('limitReached'); - } - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/facets-view.js b/server/sonar-web/src/main/js/components/navigator/facets-view.js deleted file mode 100644 index ca64a309cd8..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/facets-view.js +++ /dev/null @@ -1,47 +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 BaseFacet from './facets/base-facet'; - -export default Marionette.CollectionView.extend({ - className: 'search-navigator-facets-list', - - childViewOptions() { - return { - app: this.options.app - }; - }, - - getChildView() { - return BaseFacet; - }, - - collectionEvents() { - return { - 'change:enabled': 'updateState' - }; - }, - - updateState() { - const enabledFacets = this.collection.filter(model => model.get('enabled')); - const enabledFacetIds = enabledFacets.map(model => model.id); - this.options.app.state.set({ facets: enabledFacetIds }); - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/facets/base-facet.js b/server/sonar-web/src/main/js/components/navigator/facets/base-facet.js deleted file mode 100644 index 4cf6963a9e3..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/facets/base-facet.js +++ /dev/null @@ -1,122 +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'; - -export default Marionette.ItemView.extend({ - className: 'search-navigator-facet-box', - forbiddenClassName: 'search-navigator-facet-box-forbidden', - - modelEvents() { - return { - change: 'render' - }; - }, - - events() { - return { - 'click .js-facet-toggle': 'toggle', - 'click .js-facet': 'toggleFacet' - }; - }, - - onRender() { - this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled')); - this.$el.attr('data-property', this.model.get('property')); - const that = this; - const property = this.model.get('property'); - const value = this.options.app.state.get('query')[property]; - if (typeof value === 'string') { - value.split(',').forEach(s => { - const facet = that.$('.js-facet').filter(`[data-value="${s}"]`); - if (facet.length > 0) { - facet.addClass('active'); - } - }); - } - }, - - toggle() { - if (!this.isForbidden()) { - this.options.app.controller.toggleFacet(this.model.id); - } - }, - - getValue() { - return this.$('.js-facet.active') - .map(function() { - return $(this).data('value'); - }) - .get() - .join(); - }, - - toggleFacet(e) { - $(e.currentTarget).toggleClass('active'); - const property = this.model.get('property'); - const obj = {}; - obj[property] = this.getValue(); - this.options.app.state.updateFilter(obj); - }, - - disable() { - const property = this.model.get('property'); - const obj = {}; - obj[property] = null; - this.options.app.state.updateFilter(obj); - }, - - forbid() { - this.options.app.controller.disableFacet(this.model.id); - this.$el.addClass(this.forbiddenClassName); - }, - - allow() { - this.$el.removeClass(this.forbiddenClassName); - }, - - isForbidden() { - return this.$el.hasClass(this.forbiddenClassName); - }, - - sortValues(values) { - return values.slice().sort((left, right) => { - if (left.count !== right.count) { - return right.count - left.count; - } - if (left.val !== right.val) { - if (left.val > right.val) { - return 1; - } - if (left.val < right.val) { - return -1; - } - } - return 0; - }); - }, - - serializeData() { - return { - ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), - values: this.sortValues(this.model.getValues()) - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/models/facet.js b/server/sonar-web/src/main/js/components/navigator/models/facet.js deleted file mode 100644 index ce1e6200314..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/models/facet.js +++ /dev/null @@ -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 Backbone from 'backbone'; - -export default Backbone.Model.extend({ - idAttribute: 'property', - - defaults: { - enabled: false - }, - - getValues() { - return this.get('values') || []; - }, - - toggle() { - const enabled = this.get('enabled'); - this.set({ enabled: !enabled }); - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/models/facets.js b/server/sonar-web/src/main/js/components/navigator/models/facets.js deleted file mode 100644 index 513474c7c65..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/models/facets.js +++ /dev/null @@ -1,25 +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 Facet from './facet'; - -export default Backbone.Collection.extend({ - model: Facet -}); diff --git a/server/sonar-web/src/main/js/components/navigator/models/state.js b/server/sonar-web/src/main/js/components/navigator/models/state.js deleted file mode 100644 index 60042553b84..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/models/state.js +++ /dev/null @@ -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 Backbone from 'backbone'; - -export default Backbone.Model.extend({ - defaults() { - return { - page: 1, - maxResultsReached: false, - query: {}, - facets: [] - }; - }, - - nextPage() { - const page = this.get('page'); - this.set({ page: page + 1 }); - }, - - clearQuery(query) { - const q = {}; - Object.keys(query).forEach(key => { - if (query[key]) { - q[key] = query[key]; - } - }); - return q; - }, - - _areQueriesEqual(a, b) { - let equal = Object.keys(a).length === Object.keys(b).length; - Object.keys(a).forEach(key => { - equal = equal && a[key] === b[key]; - }); - return equal; - }, - - updateFilter(obj, options) { - const oldQuery = this.get('query'); - let query = { ...oldQuery, ...obj }; - const opts = { force: false, ...options }; - query = this.clearQuery(query); - if (opts.force || !this._areQueriesEqual(oldQuery, query)) { - this.setQuery(query); - } - }, - - setQuery(query) { - this.set({ query }, { silent: true }); - this.set({ changed: true }); - this.trigger('change:query'); - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/router.js b/server/sonar-web/src/main/js/components/navigator/router.js deleted file mode 100644 index 2ff317a5b91..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/router.js +++ /dev/null @@ -1,44 +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.Router.extend({ - routeSeparator: '|', - - routes: { - '': 'index', - ':query': 'index' - }, - - initialize(options) { - this.options = options; - this.listenTo(this.options.app.state, 'change:query', this.updateRoute); - }, - - index(query) { - query = this.options.app.controller.parseQuery(query); - this.options.app.state.setQuery(query); - }, - - updateRoute() { - const route = this.options.app.controller.getRoute(); - this.navigate(route); - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-header-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-header-view.js deleted file mode 100644 index ea09144e92b..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/workspace-header-view.js +++ /dev/null @@ -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. - */ -import Marionette from 'backbone.marionette'; - -export default Marionette.ItemView.extend({ - collectionEvents() { - return { - all: 'shouldRender', - limitReached: 'flashPagination' - }; - }, - - events() { - return { - 'click .js-bulk-change': 'onBulkChangeClick', - 'click .js-reload': 'reload', - 'click .js-next': 'selectNext', - 'click .js-prev': 'selectPrev' - }; - }, - - initialize(options) { - this.listenTo(options.app.state, 'change', this.render); - }, - - onRender() { - this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); - }, - - onBeforeRender() { - this.$('[data-toggle="tooltip"]').tooltip('destroy'); - }, - - onDestroy() { - this.$('[data-toggle="tooltip"]').tooltip('destroy'); - }, - - onBulkChangeClick(e) { - e.preventDefault(); - this.bulkChange(); - }, - - bulkChange() {}, - - shouldRender(event) { - if (event !== 'limitReached') { - this.render(); - } - }, - - reload() { - this.options.app.controller.fetchList(); - }, - - selectNext() { - this.options.app.controller.selectNext(); - }, - - selectPrev() { - this.options.app.controller.selectPrev(); - }, - - flashPagination() { - const flashElement = this.$('.search-navigator-header-pagination'); - flashElement.addClass('in'); - setTimeout(() => { - flashElement.removeClass('in'); - }, 2000); - }, - - serializeData() { - return { - ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), - state: this.options.app.state.toJSON() - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-list-item-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-list-item-view.js deleted file mode 100644 index 9c36540c836..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/workspace-list-item-view.js +++ /dev/null @@ -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 Marionette from 'backbone.marionette'; - -export default Marionette.ItemView.extend({ - initialize(options) { - this.listenTo(options.app.state, 'change:selectedIndex', this.select); - }, - - onRender() { - this.select(); - }, - - select() { - const selected = this.model.get('index') === this.options.app.state.get('selectedIndex'); - this.$el.toggleClass('selected', selected); - }, - - selectCurrent() { - this.options.app.state.set({ selectedIndex: this.model.get('index') }); - } -}); diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js deleted file mode 100644 index 6f262a3afc9..00000000000 --- a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js +++ /dev/null @@ -1,133 +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 { throttle } from 'lodash'; -import Marionette from 'backbone.marionette'; -import key from 'keymaster'; - -const BOTTOM_OFFSET = 60; - -export default Marionette.CompositeView.extend({ - ui: { - loadMore: '.js-more', - lastElementReached: '.js-last-element-reached' - }, - - childViewOptions() { - return { - app: this.options.app - }; - }, - - collectionEvents: { - reset: 'scrollToTop' - }, - - initialize(options) { - this.loadMoreThrottled = throttle(this.loadMore, 1000, { trailing: false }); - this.listenTo(options.app.state, 'change:maxResultsReached', this.toggleLoadMore); - this.listenTo(options.app.state, 'change:selectedIndex', this.scrollTo); - this.bindShortcuts(); - }, - - onDestroy() { - this.unbindScrollEvents(); - this.unbindShortcuts(); - }, - - onRender() { - this.toggleLoadMore(); - }, - - toggleLoadMore() { - const maxResultsReached = this.options.app.state.get('maxResultsReached'); - this.ui.loadMore.toggle(!maxResultsReached); - this.ui.lastElementReached.toggle(maxResultsReached); - }, - - bindScrollEvents() { - const that = this; - $(window).on('scroll.workspace-list-view', () => { - that.onScroll(); - }); - }, - - unbindScrollEvents() { - $(window).off('scroll.workspace-list-view'); - }, - - bindShortcuts() { - const that = this; - key('up', 'list', () => { - that.options.app.controller.selectPrev(); - return false; - }); - - key('down', 'list', () => { - that.options.app.controller.selectNext(); - return false; - }); - }, - - unbindShortcuts() { - key.unbind('up', 'list'); - key.unbind('down', 'list'); - }, - - loadMore() { - if (!this.options.app.state.get('maxResultsReached')) { - const that = this; - this.unbindScrollEvents(); - this.options.app.controller.fetchNextPage().then(() => { - that.bindScrollEvents(); - }); - } - }, - - onScroll() { - if ($(window).scrollTop() + $(window).height() >= this.ui.loadMore.offset().top) { - this.loadMoreThrottled(); - } - }, - - scrollToTop() { - this.$el.scrollParent().scrollTop(0); - }, - - scrollTo() { - const selected = this.collection.at(this.options.app.state.get('selectedIndex')); - if (selected == null) { - return; - } - const selectedView = this.children.findByModel(selected); - const parentTopOffset = this.$el.offset().top; - const viewTop = selectedView.$el.offset().top - parentTopOffset; - const viewBottom = - selectedView.$el.offset().top + selectedView.$el.outerHeight() + BOTTOM_OFFSET; - const windowTop = $(window).scrollTop(); - const windowBottom = windowTop + $(window).height(); - if (viewTop < windowTop) { - $(window).scrollTop(viewTop); - } - if (viewBottom > windowBottom) { - $(window).scrollTop($(window).scrollTop() - windowBottom + viewBottom); - } - } -}); diff --git a/server/sonar-web/src/main/js/components/shared/SeverityHelper.js b/server/sonar-web/src/main/js/components/shared/SeverityHelper.tsx index 217603ee4e8..40788425b7b 100644 --- a/server/sonar-web/src/main/js/components/shared/SeverityHelper.js +++ b/server/sonar-web/src/main/js/components/shared/SeverityHelper.tsx @@ -17,18 +17,22 @@ * 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 * as React from 'react'; import SeverityIcon from './SeverityIcon'; import { translate } from '../../helpers/l10n'; -export default function SeverityHelper(props /*: { severity: ?string, className?: string } */) { - const { severity } = props; +interface Props { + className?: string; + // TODO avoid passing nil values + severity: string | undefined | null; +} + +export default function SeverityHelper({ className, severity }: Props) { if (!severity) { return null; } return ( - <span className={props.className}> + <span className={className}> <SeverityIcon className="little-spacer-right" severity={severity} /> {translate('severity', severity)} </span> diff --git a/server/sonar-web/src/main/js/components/shared/TypeHelper.js b/server/sonar-web/src/main/js/components/shared/TypeHelper.tsx index ada07852156..1229bf38a88 100644 --- a/server/sonar-web/src/main/js/components/shared/TypeHelper.js +++ b/server/sonar-web/src/main/js/components/shared/TypeHelper.tsx @@ -17,22 +17,20 @@ * 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 * as React from 'react'; import IssueTypeIcon from '../ui/IssueTypeIcon'; import { translate } from '../../helpers/l10n'; -/*:: -type Props = { - type: string -}; -*/ +interface Props { + className?: string; + type: string; +} -const TypeHelper = (props /*: Props */) => ( - <span> - <IssueTypeIcon className="little-spacer-right" query={props.type} /> - {translate('issue.type', props.type)} - </span> -); - -export default TypeHelper; +export default function TypeHelper(props: Props) { + return ( + <span className={props.className}> + <IssueTypeIcon className="little-spacer-right" query={props.type} /> + {translate('issue.type', props.type)} + </span> + ); +} diff --git a/server/sonar-web/src/main/js/components/tags/TagsList.tsx b/server/sonar-web/src/main/js/components/tags/TagsList.tsx index a1fc9a39488..9b7931c4722 100644 --- a/server/sonar-web/src/main/js/components/tags/TagsList.tsx +++ b/server/sonar-web/src/main/js/components/tags/TagsList.tsx @@ -23,18 +23,15 @@ import './TagsList.css'; interface Props { allowUpdate?: boolean; - customClass?: string; + className?: string; tags: string[]; } -export default function TagsList({ allowUpdate = false, customClass, tags }: Props) { - const spanClass = classNames('text-ellipsis', { note: !allowUpdate }); - const tagListClass = classNames('tags-list', customClass); - +export default function TagsList({ allowUpdate = false, className, tags }: Props) { return ( - <span className={tagListClass} title={tags.join(', ')}> - <i className="icon-tags icon-half-transparent" /> - <span className={spanClass}>{tags.join(', ')}</span> + <span className={classNames('tags-list', className)} title={tags.join(', ')}> + <i className="icon-tags" /> + <span className="text-ellipsis">{tags.join(', ')}</span> {allowUpdate && <i className="icon-dropdown" />} </span> ); diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.tsx b/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.tsx index 2d3d8b71ef5..7b75a3aa5cf 100644 --- a/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.tsx +++ b/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.tsx @@ -24,23 +24,9 @@ import TagsList from '../TagsList'; const tags = ['foo', 'bar']; it('should render with a list of tag', () => { - const taglist = shallow(<TagsList tags={tags} />); - expect(taglist.text()).toBe(tags.join(', ')); - expect(taglist.find('i').length).toBe(1); - expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(true); -}); - -it('should correctly handle a lot of tags', () => { - const lotOfTags = []; - for (let i = 0; i < 20; i++) { - lotOfTags.push(String(tags)); - } - const taglist = shallow(<TagsList tags={lotOfTags} />); - expect(taglist.text()).toBe(lotOfTags.join(', ')); - expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(true); + expect(shallow(<TagsList tags={tags} />)).toMatchSnapshot(); }); it('should render with a caret on the right if update is allowed', () => { - const taglist = shallow(<TagsList tags={tags} allowUpdate={true} />); - expect(taglist.find('i').length).toBe(2); + expect(shallow(<TagsList allowUpdate={true} tags={tags} />)).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsList-test.tsx.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsList-test.tsx.snap new file mode 100644 index 00000000000..d4305854263 --- /dev/null +++ b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsList-test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render with a caret on the right if update is allowed 1`] = ` +<span + className="tags-list" + title="foo, bar" +> + <i + className="icon-tags" + /> + <span + className="text-ellipsis" + > + foo, bar + </span> + <i + className="icon-dropdown" + /> +</span> +`; + +exports[`should render with a list of tag 1`] = ` +<span + className="tags-list" + title="foo, bar" +> + <i + className="icon-tags" + /> + <span + className="text-ellipsis" + > + foo, bar + </span> +</span> +`; diff --git a/server/sonar-web/src/main/js/components/workspace/views/rule-view.js b/server/sonar-web/src/main/js/components/workspace/views/rule-view.js index 5ae7b8c1679..88c2dbb5b8f 100644 --- a/server/sonar-web/src/main/js/components/workspace/views/rule-view.js +++ b/server/sonar-web/src/main/js/components/workspace/views/rule-view.js @@ -21,7 +21,7 @@ import { union } from 'lodash'; import Marionette from 'backbone.marionette'; import BaseView from './base-viewer-view'; import Template from '../templates/workspace-rule.hbs'; -import { getRulesUrl } from '../../../helpers/urls'; +import { getPathUrlAsString, getRulesUrl } from '../../../helpers/urls'; import { areThereCustomOrganizations } from '../../../store/organizations/utils'; export default BaseView.extend({ @@ -38,9 +38,11 @@ export default BaseView.extend({ serializeData() { const query = { rule_key: this.model.get('key') }; - const permalink = areThereCustomOrganizations() - ? getRulesUrl(query, this.model.get('organization')) - : getRulesUrl(query); + const permalink = getPathUrlAsString( + areThereCustomOrganizations() + ? getRulesUrl(query, this.model.get('organization')) + : getRulesUrl(query, undefined) + ); return { ...Marionette.LayoutView.prototype.serializeData.apply(this, arguments), diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index 519b0473e1c..7c6a893ca7f 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -21,6 +21,8 @@ import * as theme from '../app/theme'; export const SEVERITIES = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; export const STATUSES = ['OPEN', 'REOPENED', 'CONFIRMED', 'RESOLVED', 'CLOSED']; +export const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; +export const RULE_STATUSES = ['READY', 'BETA', 'DEPRECATED']; export const CHART_COLORS_RANGE_PERCENT = [ theme.green, diff --git a/server/sonar-web/src/main/js/helpers/handlebars/empty.js b/server/sonar-web/src/main/js/helpers/handlebars/empty.js deleted file mode 100644 index 93e602e742b..00000000000 --- a/server/sonar-web/src/main/js/helpers/handlebars/empty.js +++ /dev/null @@ -1,23 +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. - */ -module.exports = function(array, options) { - const cond = Array.isArray(array) && array.length > 0; - return cond ? options.inverse(this) : options.fn(this); -}; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/gt.js b/server/sonar-web/src/main/js/helpers/handlebars/gt.js deleted file mode 100644 index bdca5f2b7fc..00000000000 --- a/server/sonar-web/src/main/js/helpers/handlebars/gt.js +++ /dev/null @@ -1,22 +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. - */ -module.exports = function(v1, v2, options) { - return v1 > v2 ? options.fn(this) : options.inverse(this); -}; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/ifLength.js b/server/sonar-web/src/main/js/helpers/handlebars/ifLength.js deleted file mode 100644 index 7d054eec948..00000000000 --- a/server/sonar-web/src/main/js/helpers/handlebars/ifLength.js +++ /dev/null @@ -1,23 +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. - */ -module.exports = function(array, len, options) { - const cond = Array.isArray(array) && array.length === +len; - return cond ? options.fn(this) : options.inverse(this); -}; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/repeat.js b/server/sonar-web/src/main/js/helpers/handlebars/repeat.js deleted file mode 100644 index 4fea69bcecc..00000000000 --- a/server/sonar-web/src/main/js/helpers/handlebars/repeat.js +++ /dev/null @@ -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. - */ -module.exports = function(number, options) { - let ret = ''; - for (let i = 0; i < number; i++) { - ret += options.fn(this); - } - return ret; -}; diff --git a/server/sonar-web/src/main/js/helpers/query.ts b/server/sonar-web/src/main/js/helpers/query.ts index 1ed0b791317..e495eb7da3a 100644 --- a/server/sonar-web/src/main/js/helpers/query.ts +++ b/server/sonar-web/src/main/js/helpers/query.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { isNil, omitBy } from 'lodash'; -import { isValidDate, parseDate, toNotSoISOString } from './dates'; +import { isValidDate, parseDate, toNotSoISOString, toShortNotSoISOString } from './dates'; export interface RawQuery { [x: string]: any; @@ -60,6 +60,16 @@ export function parseAsBoolean(value: string | undefined, defaultValue: boolean return value === 'false' ? false : value === 'true' ? true : defaultValue; } +export function parseAsOptionalBoolean(value: string | undefined): boolean | undefined { + if (value === 'true') { + return true; + } else if (value === 'false') { + return false; + } else { + return undefined; + } +} + export function parseAsDate(value?: string): Date | undefined { if (value) { const date = parseDate(value); @@ -78,6 +88,10 @@ export function parseAsString(value: string | undefined): string { return value || ''; } +export function parseAsOptionalString(value: string | undefined): string | undefined { + return value || undefined; +} + export function parseAsArray( value: string | undefined, itemParser: (x: string) => string @@ -85,17 +99,31 @@ export function parseAsArray( return value ? value.split(',').map(itemParser) : []; } -export function serializeDate(value?: Date): string | undefined { +export function serializeDate(value?: Date, serializer = toNotSoISOString): string | undefined { if (value != null && value.toISOString) { - return toNotSoISOString(value); + return serializer(value); } return undefined; } -export function serializeString(value: string): string | undefined { +export function serializeDateShort(value: Date | undefined): string | undefined { + return serializeDate(value, toShortNotSoISOString); +} + +export function serializeString(value: string | undefined): string | undefined { return value || undefined; } export function serializeStringArray(value: string[] | undefined[]): string | undefined { return value && value.length ? value.join() : undefined; } + +export function serializeOptionalBoolean(value: boolean | undefined): string | undefined { + if (value === true) { + return 'true'; + } else if (value === false) { + return 'false'; + } else { + return undefined; + } +} diff --git a/server/sonar-web/src/main/js/helpers/scrolling.ts b/server/sonar-web/src/main/js/helpers/scrolling.ts index d698665e090..86367ed4f2c 100644 --- a/server/sonar-web/src/main/js/helpers/scrolling.ts +++ b/server/sonar-web/src/main/js/helpers/scrolling.ts @@ -65,7 +65,7 @@ let smoothScrollTop = (y: number, parent: HTMLElement | Window) => { smoothScrollTop = debounce(smoothScrollTop, SCROLLING_DURATION, { leading: true }); export function scrollToElement( - element: HTMLElement, + element: Element, options: { topOffset?: number; bottomOffset?: number; diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 51666283ff8..732c0b6a6be 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -68,8 +68,9 @@ export function getProjectBranchUrl(key: string, branch: Branch): Location { /** * Generate URL for a global issues page */ -export function getIssuesUrl(query: Query): Location { - return { pathname: '/issues', query }; +export function getIssuesUrl(query: Query, organization?: string): Location { + const pathname = organization ? `/organizations/${organization}/issues` : '/issues'; + return { pathname, query }; } /** @@ -141,29 +142,28 @@ export function getQualityGatesUrl(organization?: string | null): Location { /** * Generate URL for the rules page */ -export function getRulesUrl(query: { [x: string]: string }, organization?: string | null): string { - const path = organization ? `/organizations/${organization}/rules` : '/coding_rules'; - - if (query) { - const serializedQuery = Object.keys(query) - .map(criterion => `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}`) - .join('|'); - - // return a string (not { pathname }) to help react-router's Link handle this properly - return path + '#' + serializedQuery; - } - - return path; +export function getRulesUrl(query: Query, organization: string | null | undefined): Location { + const pathname = organization ? `/organizations/${organization}/rules` : '/coding_rules'; + return { pathname, query }; } /** * Generate URL for the rules page filtering only active deprecated rules */ -export function getDeprecatedActiveRulesUrl(query = {}, organization?: string | null): string { +export function getDeprecatedActiveRulesUrl( + query: Query = {}, + organization: string | null | undefined +): Location { const baseQuery = { activation: 'true', statuses: 'DEPRECATED' }; return getRulesUrl({ ...query, ...baseQuery }, organization); } +export function getRuleUrl(rule: string, organization: string | undefined) { + /* eslint-disable camelcase */ + return getRulesUrl({ open: rule, rule_key: rule }, organization); + /* eslint-enable camelcase */ +} + export function getMarkdownHelpUrl(): string { return getBaseUrl() + '/markdown/help'; } diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 43f6f31693e..5c33dbc9e47 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -59,6 +59,10 @@ version "3.2.11" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.2.11.tgz#9119f91bb103b16ae8c4375b019a9b341b409f50" +"@types/keymaster@1.6.28": + version "1.6.28" + resolved "https://registry.yarnpkg.com/@types/keymaster/-/keymaster-1.6.28.tgz#093fc6fe49deff4ee17d36935a49230edb1c935f" + "@types/lodash@4.14.80": version "4.14.80" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.80.tgz#a6b8b7900e6a7dcbc2e90d9b6dfbe3f6a7f69951" |