]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite rules app with react (#2982)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 29 Jan 2018 13:21:28 +0000 (14:21 +0100)
committerGitHub <noreply@github.com>
Mon, 29 Jan 2018 13:21:28 +0000 (14:21 +0100)
250 files changed:
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/QProfileTester.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/QualityProfilePage.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleDetails.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RuleItem.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/RulesPage.java
server/sonar-web/package.json
server/sonar-web/src/main/js/api/issues.ts
server/sonar-web/src/main/js/api/quality-profiles.ts
server/sonar-web/src/main/js/api/rules.ts
server/sonar-web/src/main/js/app/styles/components/search-navigator.css
server/sonar-web/src/main/js/app/styles/init/forms.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/about/components/AboutStandards.js
server/sonar-web/src/main/js/apps/coding-rules/bulk-change-modal-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/bulk-change-popup-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationSeverityFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/components/ConfirmButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/InheritanceFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/PageActions.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RemoveExtendedDescriptionModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RepositoryFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleInheritanceIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/SimilarRulesFilter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/StatusFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/TemplateFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/TypeFacet.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/confirm-dialog.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/controller.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/active-severity-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/available-since-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/base-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/custom-labels-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/custom-values-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/inheritance-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/key-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/language-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/quality-profile-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/repository-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/severity-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/status-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/tag-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/template-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/facets/type-facet.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/init.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/layout.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/models/rule.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/models/rules.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/models/state.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/query.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/routes.ts
server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule-filter-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/delete-rule-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-description-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-parameters-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profile-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/styles.css
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-modal.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-bulk-change-popup.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-layout.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-details.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-rule-filter-form.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-header.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list-item.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/coding-rules-workspace-list.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/_coding-rules-facet-header.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-available-since-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-base-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-custom-values-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-inheritance-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-key-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-quality-profile-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-query-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-severity-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-template-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-type-facet.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule-creation.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rule.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-custom-rules.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-delete-rule.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-profile-activation.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-description.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-issues.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-meta.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-parameters.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profile.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/templates/rule/coding-rules-rule-profiles.hbs [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/workspace-header-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/workspace-list-empty-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/workspace-list-item-view.js [deleted file]
server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js [deleted file]
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js
server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js [deleted file]
server/sonar-web/src/main/js/apps/issues/components/IssuesCounter.js
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.js.snap
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap
server/sonar-web/src/main/js/apps/issues/styles.css
server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/routes.ts
server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js
server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesDeprecatedWarning.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRules-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesDeprecatedWarning-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowOfType-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesRowTotal-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileRulesSonarWayComparison-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.tsx
server/sonar-web/src/main/js/components/common/BubblePopup.tsx
server/sonar-web/src/main/js/components/common/DeferredSpinner.tsx
server/sonar-web/src/main/js/components/common/FiltersHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/MarkdownTips.js [deleted file]
server/sonar-web/src/main/js/components/common/MarkdownTips.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/PageCounter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/action-options-view.js [deleted file]
server/sonar-web/src/main/js/components/common/templates/_markdown-tips.hbs [deleted file]
server/sonar-web/src/main/js/components/controls/ReloadButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/SearchBox.tsx
server/sonar-web/src/main/js/components/controls/SearchSelect.js [deleted file]
server/sonar-web/src/main/js/components/controls/SearchSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap
server/sonar-web/src/main/js/components/controls/react-select.css
server/sonar-web/src/main/js/components/facet/FacetBox.js [deleted file]
server/sonar-web/src/main/js/components/facet/FacetBox.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/FacetFooter.js [deleted file]
server/sonar-web/src/main/js/components/facet/FacetFooter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/FacetHeader.js [deleted file]
server/sonar-web/src/main/js/components/facet/FacetHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/FacetItem.js [deleted file]
server/sonar-web/src/main/js/components/facet/FacetItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/FacetItemsList.js [deleted file]
server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.js [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.js [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.js [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.js [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.js [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/components/IssueTags.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
server/sonar-web/src/main/js/components/navigator/controller.js [deleted file]
server/sonar-web/src/main/js/components/navigator/facets-view.js [deleted file]
server/sonar-web/src/main/js/components/navigator/facets/base-facet.js [deleted file]
server/sonar-web/src/main/js/components/navigator/models/facet.js [deleted file]
server/sonar-web/src/main/js/components/navigator/models/facets.js [deleted file]
server/sonar-web/src/main/js/components/navigator/models/state.js [deleted file]
server/sonar-web/src/main/js/components/navigator/router.js [deleted file]
server/sonar-web/src/main/js/components/navigator/workspace-header-view.js [deleted file]
server/sonar-web/src/main/js/components/navigator/workspace-list-item-view.js [deleted file]
server/sonar-web/src/main/js/components/navigator/workspace-list-view.js [deleted file]
server/sonar-web/src/main/js/components/shared/SeverityHelper.js [deleted file]
server/sonar-web/src/main/js/components/shared/SeverityHelper.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/TypeHelper.js [deleted file]
server/sonar-web/src/main/js/components/shared/TypeHelper.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/tags/TagsList.tsx
server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.tsx
server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/workspace/views/rule-view.js
server/sonar-web/src/main/js/helpers/constants.ts
server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js [deleted file]
server/sonar-web/src/main/js/helpers/handlebars/empty.js [deleted file]
server/sonar-web/src/main/js/helpers/handlebars/gt.js [deleted file]
server/sonar-web/src/main/js/helpers/handlebars/ifLength.js [deleted file]
server/sonar-web/src/main/js/helpers/handlebars/repeat.js [deleted file]
server/sonar-web/src/main/js/helpers/query.ts
server/sonar-web/src/main/js/helpers/scrolling.ts
server/sonar-web/src/main/js/helpers/urls.ts
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/src/test/java/org/sonarqube/tests/Category6Suite.java
tests/src/test/java/org/sonarqube/tests/qualityProfile/OrganizationQualityProfilesUiTest.java
tests/src/test/java/org/sonarqube/tests/rule/RulesPageTest.java
tests/src/test/resources/organization/OrganizationQualityProfilesUiTest/should_display_list.html
tests/src/test/resources/qualityProfile/QualityProfilesUiTest/should_display_list.html

index d295cbce3e23fc192b1d9435772e9daf48c79156..2609faaa7c04e1e5de7894e648c911f98c39e26d 100644 (file)
@@ -77,6 +77,19 @@ public class QProfileTester {
     return this;
   }
 
+  public QProfileTester activateRule(QualityProfile profile, String ruleKey, String severity) {
+    return activateRule(profile.getKey(), ruleKey, severity);
+  }
+
+  public QProfileTester activateRule(String profileKey, String ruleKey, String severity) {
+    ActivateRuleRequest request = new ActivateRuleRequest()
+      .setKey(profileKey)
+      .setRule(ruleKey)
+      .setSeverity(severity);
+    service().activateRule(request);
+    return this;
+  }
+
   public QProfileTester deactivateRule(QualityProfile profile, String ruleKey) {
     service().deactivateRule(new DeactivateRuleRequest().setKey(profile.getKey()).setRule(ruleKey));
     return this;
index 1366b7f9aa31f97587725ae2fc1406c34355124b..9dbb657f824458bd81ba9d555e6d66831307b95c 100644 (file)
@@ -37,7 +37,6 @@ public class QualityProfilePage {
   public RulesPage showMissingSonarWayRules() {
     Selenide.$(".quality-profile-rules-sonarway-missing")
       .shouldBe(Condition.visible).$("a").click();
-    Selenide.$(".coding-rules").shouldBe(Condition.visible);
     return Selenide.page(RulesPage.class);
   }
 
index bbf79fc50c82cd63cf79455a468f726047c13ef6..75bfe6ccb0c34b5dc605224b8474ef3bc08bf2bb 100644 (file)
  */
 package org.sonarqube.qa.util.pageobjects;
 
-import com.codeborne.selenide.Selenide;
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+import java.util.Locale;
 
 import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
 
 public class RuleDetails {
+  RuleDetails() {
+    $(".coding-rule-details").shouldBe(visible);
+  }
+
+  public RuleDetails shouldHaveType(String type) {
+    $(".coding-rules-detail-property[data-meta=\"type\"]").shouldHave(text(type));
+    return this;
+  }
+
+  public RuleDetails shouldHaveSeverity(String severity) {
+    $(".coding-rules-detail-property[data-meta=\"severity\"]").shouldHave(text(severity));
+    return this;
+  }
+
+  public RuleDetails shouldHaveDescription(String description) {
+    $(".js-rule-description").shouldHave(text(description));
+    return this;
+  }
+
+  public RuleDetails shouldBeActivatedOn(String profileKey) {
+    $("#coding-rules-detail-quality-profiles [data-profile=\"" + profileKey + "\"]").shouldBe(visible);
+    return this;
+  }
+
+  public RuleDetails shouldNotBeActivatedOn(String profileName) {
+    $("#coding-rules-detail-quality-profiles").shouldNotHave(text(profileName));
+    return this;
+  }
+
+  public RuleDetails shouldHaveTotalIssues(int issues) {
+    $(".js-rule-issues h3").shouldHave(text(String.valueOf(issues)));
+    return this;
+  }
+
+  public RuleDetails shouldHaveIssuesOnProject(String projectName, int issues) {
+    $(".coding-rules-most-violated-projects").shouldHave(
+      Condition.and("", text(projectName), text(String.valueOf(issues))));
+    return this;
+  }
+
+  public RuleDetails shouldHaveCustomRule(String ruleKey) {
+    takeCustomRule(ruleKey).shouldBe(visible);
+    return this;
+  }
+
+  public RuleDetails shouldNotHaveCustomRule(String ruleKey) {
+    takeCustomRule(ruleKey).shouldNotBe(visible);
+    return this;
+  }
+
+  public RuleDetails createCustomRule(String ruleName) {
+    $(".js-create-custom-rule").click();
+    modal().shouldBe(visible);
+
+    $("#coding-rules-custom-rule-creation-name").val(ruleName);
+    $("#coding-rules-custom-rule-creation-html-description").val("description");
+    $("#coding-rules-custom-rule-creation-create").click();
+
+    modal().shouldNotBe(visible);
+    return this;
+  }
+
+  public RuleDetails reactivateCustomRule(String ruleName) {
+    $(".js-create-custom-rule").click();
+    modal().shouldBe(visible);
+
+    $("#coding-rules-custom-rule-creation-name").val(ruleName);
+    $("#coding-rules-custom-rule-creation-html-description").val("description");
+    $("#coding-rules-custom-rule-creation-create").click();
+
+    modal().find(".alert-warning").shouldBe(visible);
+    $("#coding-rules-custom-rule-creation-reactivate").click();
+
+    modal().shouldNotBe(visible);
+    return this;
+  }
+
+  public RuleDetails deleteCustomRule(String ruleKey) {
+    takeCustomRule(ruleKey).$(".js-delete-custom-rule").click();
+    modal().shouldBe(visible);
+    modal().find("button").click();
+    modal().shouldNotBe(visible);
+    return this;
+  }
+
+  public RuleActivation activate() {
+    $("#coding-rules-quality-profile-activate").click();
+    modal().shouldBe(visible);
+    return new RuleActivation();
+  }
+
+  private static SelenideElement modal() {
+    return $(".modal");
+  }
+
+  private static SelenideElement takeCustomRule(String ruleKey) {
+    return $("#coding-rules-detail-custom-rules tr[data-rule=\"" + ruleKey + "\"]");
+  }
+
+  private static SelenideElement getActiveProfileElement(String profileKey) {
+    return $("#coding-rules-detail-quality-profiles [data-profile=\"" + profileKey + "\"]");
+  }
+
+  public ExtendedDescription extendDescription() {
+    return new ExtendedDescription().start();
+  }
+
+  public Tags tags() {
+    return new Tags();
+  }
+
+  public RuleActivation changeActivationOn(String profileKey) {
+    getActiveProfileElement(profileKey).$(".coding-rules-detail-quality-profile-change").click();
+    modal().shouldBe(visible);
+    return new RuleActivation();
+  }
+
+  public RuleDetails activationShouldHaveParameter(String profileKey, String parameter, String value) {
+    getActiveProfileElement(profileKey).$$(".coding-rules-detail-quality-profile-parameter")
+      .findBy(Condition.and("", text(parameter), text(value)))
+      .shouldBe(visible);
+    return this;
+  }
+
+  public RuleDetails activationShouldHaveSeverity(String profileKey, String severity) {
+    getActiveProfileElement(profileKey).$(".coding-rules-detail-quality-profile-severity .icon-severity-" + severity.toLowerCase(Locale.ENGLISH)).shouldBe(visible);
+    return this;
+  }
 
-  public RuleDetails shouldBeActivatedOn(String profileName) {
-    Selenide.$("#coding-rules-detail-quality-profiles").shouldHave(text(profileName));
+  public RuleDetails revertActivationToParentDefinition(String profileKey) {
+    getActiveProfileElement(profileKey).$(".coding-rules-detail-quality-profile-revert").click();
+    modal().shouldBe(visible);
+    $(".modal button").click();
+    modal().shouldNotBe(visible);
     return this;
   }
 
+  public static class ExtendedDescription {
+    public ExtendedDescription start() {
+      $("#coding-rules-detail-extend-description").click();
+      textArea().shouldBe(visible);
+      return this;
+    }
+
+    public ExtendedDescription cancel() {
+      $("#coding-rules-detail-extend-description-cancel").click();
+      textArea().shouldNotBe(visible);
+      return this;
+    }
+
+    public ExtendedDescription type(String text) {
+      textArea().val(text);
+      return this;
+    }
+
+    public ExtendedDescription submit() {
+      $("#coding-rules-detail-extend-description-submit").click();
+      textArea().shouldNotBe(visible);
+      return this;
+    }
+
+    public ExtendedDescription remove() {
+      $("#coding-rules-detail-extend-description-remove").click();
+      modal().shouldBe(visible);
+      $("#coding-rules-detail-extend-description-remove-submit").click();
+      modal().shouldNotBe(visible);
+      textArea().shouldNotBe(visible);
+      return this;
+    }
+
+    private static SelenideElement textArea() {
+      return $("#coding-rules-detail-extend-description-text");
+    }
+  }
+
+  public static class Tags {
+    public Tags shouldHaveNoTags() {
+      element().shouldHave(text("No tags"));
+      return this;
+    }
+
+    public Tags shouldHaveTags(String... tags) {
+      for (String tag : tags) {
+        element().shouldHave(text(tag));
+      }
+      return this;
+    }
+
+    public Tags edit() {
+      element().$("button").click();
+      return this;
+    }
+
+    public Tags select(String tag) {
+      element().$$(".menu a").findBy(text(tag)).click();
+      return this;
+    }
+
+    public Tags search(String query) {
+      element().$(".search-box-input").val(query);
+      return this;
+    }
+
+    public Tags done() {
+      element().$(".search-box-input").pressEscape();
+      return this;
+    }
+
+    private static SelenideElement element() {
+      return $(".coding-rules-detail-property[data-meta=\"tags\"]");
+    }
+  }
+
+  public static class RuleActivation {
+    public RuleActivation select(String profileKey) {
+      $(".modal .js-profile .Select-input input").val(profileKey).pressEnter();
+      return this;
+    }
+
+    public RuleActivation fill(String parameter, String value) {
+      $(".modal-field input[name=\"" + parameter + "\"]").val(value);
+      return this;
+    }
+
+    public RuleActivation save() {
+      $(".modal button").click();
+      modal().shouldNotBe(visible);
+      return this;
+    }
+  }
 }
index 4084ba9815c8fb5db21405392e2af595c056f82b..a61a371ebdef5ee395214904164248d9794d1b7f 100644 (file)
@@ -21,21 +21,30 @@ package org.sonarqube.qa.util.pageobjects;
 
 import com.codeborne.selenide.SelenideElement;
 
+import static com.codeborne.selenide.Condition.visible;
+
 public class RuleItem {
 
   private final SelenideElement elt;
 
-  public RuleItem(SelenideElement elt) {
+  RuleItem(SelenideElement elt) {
     this.elt = elt;
   }
 
-  public SelenideElement getTitle() {
-    return elt.$(".coding-rule-title");
+  public RuleItem filterSimilarRules(String field) {
+    elt.$(".js-rule-filter").click();
+    elt.$(".dropdown-menu a[data-field=\"" + field + "\"]").click();
+    return this;
   }
 
-  public SelenideElement getMetadata() {
-    return elt.$(".coding-rule-meta");
+  public RuleDetails open() {
+    elt.$(".coding-rule-title a").click();
+    return new RuleDetails();
   }
 
+  public RuleItem shouldDisplayDeactivate() {
+    elt.$(".coding-rules-detail-quality-profile-deactivate").shouldBe(visible);
+    return this;
+  }
 
 }
index 77b82e3306018e1ba7802b604a625151a8a80f65..1e4e0605bf6265458f17efc482e285d7b28a698b 100644 (file)
@@ -21,46 +21,119 @@ package org.sonarqube.qa.util.pageobjects;
 
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.ElementsCollection;
-import com.codeborne.selenide.Selenide;
 import com.codeborne.selenide.SelenideElement;
-import org.openqa.selenium.By;
+
+import static com.codeborne.selenide.Condition.exist;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
 
 public class RulesPage extends Navigation {
 
   public RulesPage() {
-    Selenide.$(By.cssSelector(".coding-rules")).should(Condition.exist);
+    $("#coding-rules-page").should(exist);
   }
 
   public int getTotal() {
     // warning - number is localized
-    return Integer.parseInt(Selenide.$("#coding-rules-total").text());
+    return Integer.parseInt($("#coding-rules-total").text());
   }
 
   public ElementsCollection getSelectedFacetItems(String facetName) {
-    SelenideElement facet = Selenide.$(".search-navigator-facet-box[data-property='"+ facetName+"']").shouldBe(Condition.visible);
-    return facet.$$(".js-facet.active");
+    return getFacetElement(facetName).$$(".facet.active");
   }
 
   public RulesPage shouldHaveTotalRules(Integer total) {
-    Selenide.$("#coding-rules-total").shouldHave(Condition.text(total.toString()));
+    $(".js-page-counter-total").shouldHave(Condition.text(total.toString()));
+    return this;
+  }
+
+  public RulesPage shouldDisplayRules(String... ruleKeys) {
+    for (String key : ruleKeys) {
+      getRuleElement(key).shouldBe(visible);
+    }
+    return this;
+  }
+
+  public RulesPage shouldNotDisplayRules(String... ruleKeys) {
+    for (String key : ruleKeys) {
+      getRuleElement(key).shouldNotBe(visible);
+    }
     return this;
   }
 
   public RulesPage openFacet(String facet) {
-    Selenide.$(".search-navigator-facet-box[data-property=\"" + facet + "\"] .js-facet-toggle").click();
+    getFacetElement(facet).$(".search-navigator-facet-header a").click();
+    return this;
+  }
+
+  public RulesPage selectFacetItem(String facet, String value) {
+    getFacetElement(facet).$(".facet[data-facet=\"" + value + "\"]").click();
     return this;
   }
 
-  public RulesPage selectFacetItemByText(String facet, String itemText) {
-    Selenide.$$(".search-navigator-facet-box[data-property=\"" + facet + "\"] .js-facet")
-      .findBy(Condition.text(itemText)).click();
+  public RulesPage selectInactive() {
+    getFacetElement("profile").$(".active .js-inactive").click();
+    return this;
+  }
+
+  public RulesPage shouldHaveDisabledFacet(String facet) {
+    $(".search-navigator-facet-box-forbidden[data-property=\"" + facet + "\"]").shouldBe(visible);
+    return this;
+  }
+
+  public RulesPage shouldNotHaveDisabledFacet(String facet) {
+    $(".search-navigator-facet-box-forbidden[data-property=\"" + facet + "\"]").shouldNotBe(visible);
     return this;
   }
 
   public RuleDetails openFirstRule() {
-    Selenide.$$(".js-rule").first().click();
-    Selenide.$(".coding-rules-details").shouldBe(Condition.visible);
+    $$(".coding-rule-title a").first().click();
     return new RuleDetails();
   }
 
+  public RuleItem takeRule(String ruleKey) {
+    return new RuleItem(getRuleElement(ruleKey));
+  }
+
+  public RulesPage search(String query) {
+    $("#coding-rules-search .search-box-input").val(query);
+    return this;
+  }
+
+  public RulesPage clearAllFilters() {
+    $("#coding-rules-clear-all-filters").click();
+    return this;
+  }
+
+  public RulesPage closeDetails() {
+    $(".js-back").click();
+    $(".coding-rule-details").shouldNotBe(visible);
+    return this;
+  }
+
+  public RulesPage activateRule(String ruleKey) {
+    getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-activate").click();
+    $(".modal").shouldBe(visible);
+    $(".modal button").click();
+    $(".modal").shouldNotBe(visible);
+    getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-activate").shouldNotBe(visible);
+    return this;
+  }
+
+  public RulesPage deactivateRule(String ruleKey) {
+    getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-deactivate").click();
+    $(".modal button").click();
+    getRuleElement(ruleKey).$(".coding-rules-detail-quality-profile-deactivate").shouldNotBe(visible);
+    return this;
+  }
+
+  private static SelenideElement getRuleElement(String key) {
+    return $(".coding-rule[data-rule=\"" + key + "\"]");
+  }
+
+  private static SelenideElement getFacetElement(String facet) {
+    return $(".search-navigator-facet-box[data-property=\"" + facet + "\"]");
+  }
+
 }
index 1be2cba9f28c593b8827696f249d0490ec5e3e75..8089b41e56292115c064f8162be527ad41a850ac 100644 (file)
@@ -51,6 +51,7 @@
     "@types/escape-html": "0.0.20",
     "@types/jest": "22.0.1",
     "@types/jquery": "3.2.11",
+    "@types/keymaster": "1.6.28",
     "@types/lodash": "4.14.80",
     "@types/prop-types": "15.5.2",
     "@types/react": "16.0.29",
index 765dcb95efbb026512fa01d3f43516e1375a6cdc..de740e4190c8f6dcea10f4c7bfb6fcd811bb8136 100644 (file)
@@ -28,7 +28,7 @@ export interface IssueResponse {
 }
 
 interface IssuesResponse {
-  components?: Array<{}>;
+  components?: { key: string; name: string; uuid: string }[];
   debtTotal?: number;
   facets: Array<{}>;
   issues: RawIssue[];
@@ -38,7 +38,7 @@ interface IssuesResponse {
     total: number;
   };
   rules?: Array<{}>;
-  users?: Array<{ login: string }>;
+  users?: { login: string }[];
 }
 
 export function searchIssues(query: RequestData): Promise<IssuesResponse> {
@@ -57,7 +57,10 @@ export function getFacets(query: RequestData, facets: string[]): Promise<any> {
   });
 }
 
-export function getFacet(query: RequestData, facet: string): Promise<any> {
+export function getFacet(
+  query: RequestData,
+  facet: string
+): Promise<{ facet: { count: number; val: string }[]; response: IssuesResponse }> {
   return getFacets(query, [facet]).then(r => {
     return { facet: r.facets[0].values, response: r.response };
   });
@@ -82,6 +85,18 @@ export function getAssignees(query: RequestData): Promise<any> {
   return getFacet(query, 'assignees').then(r => extractAssignees(r.facet, r.response));
 }
 
+export function extractProjects(facet: { val: string }[], response: IssuesResponse) {
+  return facet.map(item => {
+    const project =
+      response.components && response.components.find(component => component.uuid === item.val);
+    return { ...item, project };
+  });
+}
+
+export function getProjects(query: RequestData) {
+  return getFacet(query, 'projectUuids').then(r => extractProjects(r.facet, r.response));
+}
+
 export function getIssuesCount(query: RequestData): Promise<any> {
   const data = { ...query, ps: 1, facetMode: 'effort' };
   return searchIssues(data).then(r => {
index 80dd491a75e51428b2d8ffc591de1d75b0bf7a07..188651ddc31b048aa0f48cdf12a8cbeea56c72d2 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { map } from 'lodash';
+import { csvEscape } from '../helpers/csv';
 import {
   request,
   checkStatus,
@@ -219,3 +221,58 @@ export function addGroup(parameters: AddRemoveGroupParameters): Promise<void | R
 export function removeGroup(parameters: AddRemoveGroupParameters): Promise<void | Response> {
   return post('/api/qualityprofiles/remove_group', parameters).catch(throwGlobalError);
 }
+
+export interface BulkActivateParameters {
+  /* eslint-disable camelcase */
+  activation?: boolean;
+  active_severities?: string;
+  asc?: boolean;
+  available_since?: string;
+  compareToProfile?: string;
+  inheritance?: string;
+  is_template?: string;
+  languages?: string;
+  organization: string | undefined;
+  q?: string;
+  qprofile?: string;
+  repositories?: string;
+  rule_key?: string;
+  s?: string;
+  severities?: string;
+  statuses?: string;
+  tags?: string;
+  targetKey: string;
+  targetSeverity?: string;
+  template_key?: string;
+  types?: string;
+  /* eslint-enable camelcase */
+}
+
+export function bulkActivateRules(data: BulkActivateParameters) {
+  return postJSON('api/qualityprofiles/activate_rules', data);
+}
+
+export function bulkDeactivateRules(data: BulkActivateParameters) {
+  return postJSON('api/qualityprofiles/deactivate_rules', data);
+}
+
+export function activateRule(data: {
+  key: string;
+  organization: string | undefined;
+  params?: { [key: string]: string };
+  reset?: boolean;
+  rule: string;
+  severity?: string;
+}) {
+  const params =
+    data.params && map(data.params, (value, key) => `${key}=${csvEscape(value)}`).join(';');
+  return post('/api/qualityprofiles/activate_rule', { ...data, params }).catch(throwGlobalError);
+}
+
+export function deactivateRule(data: {
+  key: string;
+  organization: string | undefined;
+  rule: string;
+}) {
+  return post('/api/qualityprofiles/deactivate_rule', data).catch(throwGlobalError);
+}
index 1a29eb863e8e673b01cc32f96cf7bc3ec741f4b8..ea42bf0a1a7c25b0b09a5953abaacb3e9e0441df 100644 (file)
  * 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);
+}
index f8b212776539f2475af9d7bf027e7d6d670deb5e..c997af3dae1c6ac5eb121abedb1c8b10b966271f 100644 (file)
@@ -99,7 +99,6 @@
 
 .search-navigator-facet-box-forbidden .search-navigator-facet-header {
   color: var(--secondFontColor);
-  font-weight: 400;
 }
 
 .search-navigator-facet-box-forbidden .search-navigator-facet-header:hover {
@@ -484,7 +483,7 @@ a.search-navigator-facet:focus .facet-stat {
 }
 
 .search-navigator-facet-list {
-  padding-bottom: 10px;
+  padding-bottom: var(--gridSize);
   font-size: 0;
 }
 
@@ -498,7 +497,7 @@ a.search-navigator-facet:focus .facet-stat {
 
 .search-navigator-facet-footer {
   display: block;
-  padding: 6px 10px;
+  padding-bottom: var(--gridSize);
   border-bottom: none;
 }
 
@@ -689,8 +688,9 @@ a.search-navigator-facet:focus .facet-stat {
 }
 
 .search-navigator-filters-header {
-  float: left;
-  line-height: 22px;
+  margin-bottom: 12px;
+  padding-bottom: 11px;
+  border-bottom: 1px solid var(--barBorderColor);
 }
 
 .search-navigator-filters-name {
index 9abdbddf828349a69d8efc1a8224dfc50e14c086..beb0204de27f345d7644178580088f523c16b492 100644 (file)
@@ -177,11 +177,11 @@ button:disabled:focus,
 .button:disabled:focus,
 input[type='submit']:disabled:focus,
 input[type='button']:disabled:focus {
-  color: #bbb;
-  border-color: #ddd;
-  background: #ebebeb;
-  cursor: not-allowed;
-  box-shadow: none;
+  color: #bbb !important;
+  border-color: #ddd !important;
+  background: #ebebeb !important;
+  cursor: not-allowed !important;
+  box-shadow: none !important;
 }
 
 .button svg {
index dedfabe68c2d2a059c2b0c2ee1e63f084fdf87d5..ef1f97b8cf14a8407aba4db2456db1f2d5cce59a 100644 (file)
  * 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'
+}
index 40b5093a8eaf736b1f87ac6a41b5061f454893dc..a4848285d9d2bb8c919fc3e75c50ed6e56c6b080 100644 (file)
@@ -41,7 +41,7 @@ type Props = {
 export default function AboutStandards(props /*: Props */) {
   const organization = props.appState.organizationsEnabled
     ? props.appState.defaultOrganization
-    : null;
+    : undefined;
 
   return (
     <div className="boxed-group">
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 (file)
index 1708a0c..0000000
+++ /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 (file)
index 00b686f..0000000
+++ /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 (file)
index 0000000..b96a251
--- /dev/null
@@ -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 (file)
index 0000000..2fcc083
--- /dev/null
@@ -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 (file)
index 0000000..d9dcbf7
--- /dev/null
@@ -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 (file)
index 0000000..2afc90b
--- /dev/null
@@ -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 (file)
index 0000000..b0a1815
--- /dev/null
@@ -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 (file)
index 0000000..37efe2e
--- /dev/null
@@ -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 (file)
index 0000000..5436807
--- /dev/null
@@ -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 (file)
index 3ec7c42..0000000
+++ /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 (file)
index 0000000..775fe1a
--- /dev/null
@@ -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 (file)
index 0000000..b2e9c8d
--- /dev/null
@@ -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 (file)
index 0000000..b650c27
--- /dev/null
@@ -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/components/DefaultSeverityFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/DefaultSeverityFacet.tsx
new file mode 100644 (file)
index 0000000..e9b7ef5
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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';
+
+export default class DefaultSeverityFacet extends React.PureComponent<BasicProps> {
+  renderName = (severity: string) => <SeverityHelper severity={severity} />;
+
+  renderTextName = (severity: string) => translate('severity', severity);
+
+  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 (file)
index 0000000..dd0a053
--- /dev/null
@@ -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 (file)
index 0000000..e384fcb
--- /dev/null
@@ -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 (file)
index 0000000..c4c1102
--- /dev/null
@@ -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 (file)
index 0000000..057d519
--- /dev/null
@@ -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 (file)
index 0000000..34b27b5
--- /dev/null
@@ -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 (file)
index 0000000..3341efb
--- /dev/null
@@ -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 (file)
index 0000000..402b971
--- /dev/null
@@ -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 (file)
index 0000000..95a7ee2
--- /dev/null
@@ -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 (file)
index 0000000..a32e729
--- /dev/null
@@ -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 (file)
index 0000000..d9c9007
--- /dev/null
@@ -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 (file)
index 0000000..9d7cd52
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import { sortBy } from 'lodash';
+import ConfirmButton from './ConfirmButton';
+import CustomRuleButton from './CustomRuleButton';
+import { searchRules, deleteRule } from '../../../api/rules';
+import { Rule, RuleDetails } from '../../../app/types';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import SeverityHelper from '../../../components/shared/SeverityHelper';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { getRuleUrl } from '../../../helpers/urls';
+
+interface Props {
+  canChange?: boolean;
+  organization: string | undefined;
+  ruleDetails: RuleDetails;
+}
+
+interface State {
+  loading: boolean;
+  rules?: Rule[];
+}
+
+export default class RuleDetailsCustomRules extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { loading: false };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchRules();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.ruleDetails.key !== this.props.ruleDetails.key) {
+      this.fetchRules();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchRules = () => {
+    this.setState({ loading: true });
+    searchRules({
+      f: 'name,severity,params',
+      organization: this.props.organization,
+      /* eslint-disable camelcase */
+      template_key: this.props.ruleDetails.key
+      /* eslint-enable camelcase */
+    }).then(
+      ({ rules }) => {
+        if (this.mounted) {
+          this.setState({ rules, loading: false });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleRuleCreate = (newRuleDetails: RuleDetails) => {
+    if (this.mounted) {
+      this.setState(({ rules = [] }: State) => ({
+        rules: [...rules, newRuleDetails]
+      }));
+    }
+  };
+
+  handleRuleDelete = (ruleKey: string) => {
+    return deleteRule({ key: ruleKey, organization: this.props.organization }).then(() => {
+      if (this.mounted) {
+        this.setState(({ rules = [] }) => ({
+          rules: rules.filter(rule => rule.key !== ruleKey)
+        }));
+      }
+    });
+  };
+
+  renderRule = (rule: Rule) => (
+    <tr key={rule.key} data-rule={rule.key}>
+      <td className="coding-rules-detail-list-name">
+        <Link to={getRuleUrl(rule.key, this.props.organization)}>{rule.name}</Link>
+      </td>
+
+      <td className="coding-rules-detail-list-severity">
+        <SeverityHelper severity={rule.severity} />
+      </td>
+
+      <td className="coding-rules-detail-list-parameters">
+        {rule.params &&
+          rule.params.filter(param => param.defaultValue).map(param => (
+            <div className="coding-rules-detail-list-parameter" key={param.key}>
+              <span className="key">{param.key}</span>
+              <span className="sep">:&nbsp;</span>
+              <span className="value" title={param.defaultValue}>
+                {param.defaultValue}
+              </span>
+            </div>
+          ))}
+      </td>
+
+      {this.props.canChange && (
+        <td className="coding-rules-detail-list-actions">
+          <ConfirmButton
+            confirmButtonText={translate('delete')}
+            confirmData={rule.key}
+            isDestructive={true}
+            modalBody={translateWithParameters('coding_rules.delete.custom.confirm', rule.name)}
+            modalHeader={translate('coding_rules.delete_rule')}
+            onConfirm={this.handleRuleDelete}>
+            {({ onClick }) => (
+              <button className="button-red js-delete-custom-rule" onClick={onClick}>
+                {translate('delete')}
+              </button>
+            )}
+          </ConfirmButton>
+        </td>
+      )}
+    </tr>
+  );
+
+  render() {
+    const { loading, rules = [] } = this.state;
+
+    return (
+      <div className="js-rule-custom-rules coding-rule-section">
+        <div className="coding-rules-detail-custom-rules-section">
+          <div className="coding-rule-section-separator" />
+
+          <h3 className="coding-rules-detail-title">{translate('coding_rules.custom_rules')}</h3>
+
+          {this.props.canChange && (
+            <CustomRuleButton
+              onDone={this.handleRuleCreate}
+              organization={this.props.organization}
+              templateRule={this.props.ruleDetails}>
+              {({ onClick }) => (
+                <button className="js-create-custom-rule spacer-left" onClick={onClick}>
+                  {translate('coding_rules.create')}
+                </button>
+              )}
+            </CustomRuleButton>
+          )}
+
+          <DeferredSpinner loading={loading}>
+            {rules.length > 0 && (
+              <table id="coding-rules-detail-custom-rules" className="coding-rules-detail-list">
+                <tbody>{sortBy(rules, rule => rule.name).map(this.renderRule)}</tbody>
+              </table>
+            )}
+          </DeferredSpinner>
+        </div>
+      </div>
+    );
+  }
+}
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 (file)
index 0000000..c5e31bc
--- /dev/null
@@ -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 (file)
index 0000000..78b71bb
--- /dev/null
@@ -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 (file)
index 0000000..3728d1e
--- /dev/null
@@ -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 (file)
index 0000000..a738565
--- /dev/null
@@ -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 (file)
index 0000000..f8dce62
--- /dev/null
@@ -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 (file)
index 0000000..895ab60
--- /dev/null
@@ -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 (file)
index 0000000..264d79f
--- /dev/null
@@ -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 (file)
index 0000000..2c97ea8
--- /dev/null
@@ -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 (file)
index 0000000..ea93d95
--- /dev/null
@@ -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/apps/coding-rules/components/StatusFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/StatusFacet.tsx
new file mode 100644 (file)
index 0000000..79752f3
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 { RULE_STATUSES } from '../../../helpers/constants';
+import { translate } from '../../../helpers/l10n';
+
+export default class StatusFacet extends React.PureComponent<BasicProps> {
+  renderName = (status: string) => translate('rules.status', status.toLowerCase());
+
+  render() {
+    return (
+      <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 (file)
index 0000000..9ca37cb
--- /dev/null
@@ -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 (file)
index 0000000..4a5c87d
--- /dev/null
@@ -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/coding-rules/components/TypeFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/TypeFacet.tsx
new file mode 100644 (file)
index 0000000..8094cf5
--- /dev/null
@@ -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 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);
+
+  render() {
+    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 (file)
index bb9f70c..0000000
+++ /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 (file)
index d1d84a4..0000000
+++ /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 (file)
index 2b38da2..0000000
+++ /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 (file)
index 124a1a8..0000000
+++ /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 (file)
index 1eec552..0000000
+++ /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 (file)
index b87d88d..0000000
+++ /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 (file)
index 903a172..0000000
+++ /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 (file)
index a5dd6c6..0000000
+++ /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 (file)
index 805f08a..0000000
+++ /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 (file)
index 7f8615a..0000000
+++ /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 (file)
index 5ab14fe..0000000
+++ /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 (file)
index 27e3396..0000000
+++ /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 (file)
index 3f75078..0000000
+++ /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 (file)
index a7c91d5..0000000
+++ /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 (file)
index b361118..0000000
+++ /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/status-facet.js b/server/sonar-web/src/main/js/apps/coding-rules/facets/status-facet.js
deleted file mode 100644 (file)
index ced74ad..0000000
+++ /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 { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import { translate } from '../../../helpers/l10n';
-
-export default BaseFacet.extend({
-  statuses: ['READY', 'DEPRECATED', 'BETA'],
-
-  getValues() {
-    const values = this.model.getValues();
-    return values.map(value => ({
-      ...value,
-      label: translate('rules.status', value.val.toLowerCase())
-    }));
-  },
-
-  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())
-    };
-  }
-});
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 (file)
index 59bfc9a..0000000
+++ /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 (file)
index ffd1bbe..0000000
+++ /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 (file)
index d3eeaac..0000000
+++ /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 (file)
index 2850482..0000000
+++ /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 (file)
index 5b5b51a..0000000
+++ /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 (file)
index 1f41810..0000000
+++ /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 (file)
index 29c06c8..0000000
+++ /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 (file)
index f4786b6..0000000
+++ /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 (file)
index 0000000..c893b5f
--- /dev/null
@@ -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;
+}
index 354feca37a605e7db029cf0752813f6c96959e1c..260a9db516a47f987b34a7f87fa51d0a4923708f 100644 (file)
  * 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 (file)
index 9315076..0000000
+++ /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 (file)
index 4dfd110..0000000
+++ /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 (file)
index 515b292..0000000
+++ /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 (file)
index 072347a..0000000
+++ /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 (file)
index 711699a..0000000
+++ /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 (file)
index d26d168..0000000
+++ /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 (file)
index c90a93d..0000000
+++ /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 (file)
index a7b693e..0000000
+++ /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 (file)
index 4294b47..0000000
+++ /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 (file)
index 297057a..0000000
+++ /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 (file)
index c787635..0000000
+++ /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 (file)
index a140467..0000000
+++ /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 (file)
index 176a36f..0000000
+++ /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 (file)
index 0b8122e..0000000
+++ /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
-    };
-  }
-});
index eb42a0975654b476bbc2d88ff34b067a3e0f952c..ceef209eeeacfb9a51c10cae4d183aeed6ae5fd0 100644 (file)
 
 .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 (file)
index 8816b6c..0000000
+++ /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 (file)
index c5a4622..0000000
+++ /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'}}&#8230;
-    </a>
-  </li>
-
-  {{#if allowActivateOnProfile}}
-    <li>
-      <a class="js-bulk-change" data-action="activate" data-param="{{qualityProfile}}">
-        {{t 'coding_rules.activate_in'}} <strong>{{qualityProfileName}}</strong>
-      </a>
-    </li>
-  {{/if}}
-
-
-
-  {{! deactivation }}
-
-  <li>
-    <a class="js-bulk-change" data-action="deactivate">
-      {{t 'coding_rules.deactivate_in'}}&#8230;
-    </a>
-  </li>
-
-  {{#if allowDeactivateOnProfile}}
-    <li>
-      <a class="js-bulk-change" data-action="deactivate" data-param="{{qualityProfile}}">
-        {{tp 'coding_rules.deactivate_in'}} <strong>{{qualityProfileName}}</strong>
-      </a>
-    </li>
-  {{/if}}
-</ul>
-
-
-<div class="bubble-popup-arrow"></div>
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 (file)
index 51efa6c..0000000
+++ /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 (file)
index 9df22ae..0000000
+++ /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 (file)
index 20076a4..0000000
+++ /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>&nbsp;{{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 (file)
index 900951b..0000000
+++ /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 (file)
index 4f87b2c..0000000
+++ /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>
-          &nbsp;&nbsp;&nbsp;
-        {{/notEq}}
-        <span class="note">{{langName}}</span>
-        &nbsp;&nbsp;&nbsp;
-        <span class="note" data-toggle="tooltip" data-placement="bottom"
-              title="{{t 'coding_rules.type.tooltip' this.type}}">
-          {{issueTypeIcon this.type}} {{issueType this.type}}
-        </span>
-        {{#notEmpty tags}}
-          &nbsp;&nbsp;&nbsp;
-          <i class="icon-tags"></i>
-          <span class="note">{{join tags ', '}}</span>
-        {{/notEmpty}}
-        <a class="js-rule-filter link-no-underline spacer-left" href="#">
-          <i class="icon-filter icon-half-transparent"></i>&nbsp;<i class="icon-dropdown"></i>
-        </a>
-      </div>
-    </td>
-
-    {{#any activation selectedProfile}}
-      {{#if canEditQualityProfile}}
-        {{#unless isSelectedProfileBuiltIn}}
-          <td class="coding-rule-table-meta-cell coding-rule-activation-actions">
-            {{#if activation}}
-              <button class="coding-rules-detail-quality-profile-deactivate button-red" 
-                      {{#notEq activation.inherit 'NONE'}}disabled  title="{{t 'coding_rules.can_not_deactivate'}}"{{/notEq}}>
-                {{t 'coding_rules.deactivate'}}
-              </button>
-            {{else}}
-              {{#unless isTemplate}}
-                <button class="coding-rules-detail-quality-profile-activate">{{t 'coding_rules.activate'}}</button>
-              {{/unless}}
-            {{/if}}
-          </td>
-        {{/unless}}
-      {{/if}}
-    {{/any}}
-  </tr>
-</table>
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 (file)
index cae9c96..0000000
+++ /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 (file)
index 26ba41a..0000000
+++ /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 (file)
index db6138c..0000000
+++ /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 (file)
index c9b5d3d..0000000
+++ /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 (file)
index f527fac..0000000
+++ /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 (file)
index b774078..0000000
+++ /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 (file)
index 1cc7b5b..0000000
+++ /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 (file)
index 23caeed..0000000
+++ /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 (file)
index c8f3d84..0000000
+++ /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 (file)
index 972a260..0000000
+++ /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 (file)
index ef7dd5b..0000000
+++ /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 (file)
index cfd295f..0000000
+++ /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 (file)
index 3895224..0000000
+++ /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 (file)
index 32fff86..0000000
+++ /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}}&nbsp;{{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">:&nbsp;</span><span class="value" title="{{value}}">{{defaultValue}}</span>
-      </div>
-    {{/if}}
-  {{/each}}
-  &nbsp;
-</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 (file)
index 402c99a..0000000
+++ /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 (file)
index 4c9a7f3..0000000
+++ /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 (file)
index e2c8737..0000000
+++ /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}}&nbsp;&nbsp;&nbsp;{{/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 (file)
index 17b3e98..0000000
+++ /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 (file)
index 32f822b..0000000
+++ /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 (file)
index 4483e6e..0000000
+++ /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>&nbsp;<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}}&nbsp;{{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 (file)
index 0298c2a..0000000
+++ /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 (file)
index c0b2bfe..0000000
+++ /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'}}&nbsp;{{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">:&nbsp;</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'}}&nbsp;<span class="value">{{original}}</span>
-            </div>
-          {{/notEq}}{{/if}}
-        </div>
-      {{/each}}
-      &nbsp;
-    </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 (file)
index 44131fc..0000000
+++ /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 (file)
index 703d8b7..0000000
+++ /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 (file)
index 31dfa26..0000000
+++ /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 (file)
index 9994a03..0000000
+++ /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 (file)
index 2baba45..0000000
+++ /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;
-    });
-  }
-});
index 8c7eeff56224e74f81ac792816e50fa01830d8f7..3fd472649cdc2ff76e03f8da54640aa41a278a00 100644 (file)
@@ -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)}
index b26aae7a06eca228d0c4bbecda251a4e01bb415d..723551831bbc50691a9f07096d61e15fd858ed67 100644 (file)
@@ -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}
index 9c88f409c2deb303dd84659c4c8ca08355aac2e8..864073e6026f780e9afaf8388800ea8a81f955b5 100644 (file)
@@ -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]}
index 4b9c092a2754dc94a64da7cf9b05021d4cafb863..81261fe01bfc77a687283738079d9d4a67de4b06 100644 (file)
@@ -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';
index 6ac8ce8b88f39dab3e0c1c5a949ca5e017e09102..3d5fc68f37e60c009a56c042f7d7bd460099d6fb 100644 (file)
@@ -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/FiltersHeader.js b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
deleted file mode 100644 (file)
index c0f0e8b..0000000
+++ /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.
- */
-// @flow
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = {
-  displayReset: boolean,
-  onReset: () => void
-};
-*/
-
-export default class FiltersHeader extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleResetClick = (e /*: Event & { currentTarget: HTMLElement } */) => {
-    e.preventDefault();
-    e.currentTarget.blur();
-    this.props.onReset();
-  };
-
-  render() {
-    return (
-      <div className="issues-filters-header">
-        {this.props.displayReset && (
-          <div className="pull-right">
-            <button className="button-red" onClick={this.handleResetClick}>
-              {translate('clear_all_filters')}
-            </button>
-          </div>
-        )}
-
-        <h3>{translate('filters')}</h3>
-      </div>
-    );
-  }
-}
index ea4f6e3a3db5db094adb93d9455da04d38cbd958..3af7ff5ffcda98cce8e8041a55d78277c269ec2a 100644 (file)
@@ -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}
+    />
+  );
+}
index 0af64820202ba2978168d2fe0e40d87293ce8379..2d597c7ccae419884f150db938c8171d82b4ef92 100644 (file)
@@ -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}
+/>
 `;
index ba1d37035888c050a47e0e9859e1694204d92483..f48516ee94f1110d2745851853e17387e7968f36 100644 (file)
@@ -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}
index 1953d6c474b74da9ccb3f204d622cd02366882ef..734a9f23699bd82b55160b66b691ef6a7b7d3fcc 100644 (file)
@@ -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}
index afa67153eb8a49f2e16417ab232ee48ccbeb7fd9..b3488fdb9750725c51ea42013ebe0e4e52055c3d 100644 (file)
@@ -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}
index 33151732404c888a3e416ab1a0952b81b875210e..83926732bf2cd963a19e94ec0a5df29a2fdcbbe0 100644 (file)
@@ -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}
index 1358a64bfcfc6fcd725b6203e68ffeacdc13f82e..893aaa22aa967121d9fd0843fa6217514ff0f8e4 100644 (file)
@@ -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>
index ec8e9d5980c76be5c7ae99cdaf1f401f66a26a5e..9d2587536a30614178fd747500b0717ef371748d 100644 (file)
@@ -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}
index 369d97e2f4aae7bf1ce8ac9a7df4fd4a1648f8d7..b7f8bf39d81586c36bcfcd6a222cd774af38f060 100644 (file)
@@ -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}
index c69d646a2cf71229baf35351d985bbcd1644f8d3..8a9a9a7cc8efe20a6785bf2a9ebbee0a2662c08f 100644 (file)
@@ -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}
index f61aba20e701328ffa3fd98977b1d1056bf6a61a..e93c288fbfe6230a32fab949ccf3c1ef92e10c91 100644 (file)
@@ -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}
index 3f996e04fb41885f5754ffaa8073897e06ff5f50..bd59288b2780cc88b17ec79851e7873053217f9a 100644 (file)
@@ -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}
index dd92faeaa57476580144e42e63565dbb4b3964dc..daf05e65b5826c2632b02d436cfdbbc238e6eb6d 100644 (file)
@@ -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}
index 2353ea8d480dd2869e2949995ddc9195b739c76a..a1ec5af4071aa7f2933289904adbe79ac405967e 100644 (file)
@@ -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}
index a512f09364eea7ac3f177bac49bab041320c1aa3..af3f2f6a20c7bdf63ef10f3d11681a1ee6e6f368 100644 (file)
@@ -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}
index 4bbb041ab499762861f2ebff020cba702b0aa310..d11062c7cc256240358a5692c730012a1f76c66f 100644 (file)
@@ -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}
index 51465f5a61b8deb4a6207e02457a062c41e6793b..e02ea9f64cbcbdcdb5152b4253270120088cbdb0 100644 (file)
@@ -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}
index d384037257b435a180a2ce36f2ae73743c81547a..a0afcc2cf9a942bf858bdd716d96b880de51aeb4 100644 (file)
@@ -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]}
index 61761cb4f377a3a3138bf5e249f06f1d38702142..e6ad09ee7acdea72820739e0b8bb5a4235711922 100644 (file)
   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;
   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/components/OrganizationRules.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationRules.js
deleted file mode 100644 (file)
index d7f150f..0000000
+++ /dev/null
@@ -1,28 +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 CodingRulesAppContainer from '../../coding-rules/components/CodingRulesAppContainer';
-
-export default class OrganizationRules extends React.PureComponent {
-  render() {
-    return <CodingRulesAppContainer {...this.props} />;
-  }
-}
index 159cc9ec2ebbec8958deff154c26587190378a24..e6d2aa793adfaae4e388cd5feb79219e6b40f005 100644 (file)
@@ -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',
index b84da6cb1f8676261db7872e805d68beabf2b7dc..d05bdee189323157442c1dc2bdfea3f4a2ffd86b 100644 (file)
@@ -94,6 +94,7 @@ export default class Meta extends React.PureComponent<Props> {
           <MetaQualityProfiles
             component={component}
             customOrganizations={organizationsEnabled}
+            organization={component.organization}
             profiles={qualityProfiles}
           />
         )}
index 9b08058c508d89a621a362353ee66aa5b9da2524..c39bcf3846d2e0102d7daeab4b9a99054fe10ec6 100644 (file)
@@ -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);
   }
index f08c08252feb4c60d852ece9da80d5afb36d5662..0e22da396916a046c5ac2f9a2bf81b87c277084b 100644 (file)
@@ -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>
       );
     }
index 875302462d597ce3b7496b7b83dfb06e2d1ee255..f7c3da2dfca276c19353afb8756bea858706fc5d 100644 (file)
@@ -108,6 +108,7 @@ exports[`should render without tags and admin rights 1`] = `
 >
   <TagsList
     allowUpdate={false}
+    className="note"
     tags={
       Array [
         "no_tags",
index 44bb62dea41b16d217cf841e7ab42962ecb6edb3..2c990df3673ab0354c4d04ee556937e52b43507c 100644 (file)
@@ -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 && (
index 104be36dca3ed092d9e3b7747e9c919b1685d41a..275aa8fb49afe8b951ce94d5d5ee7b50c22307ec 100644 (file)
@@ -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">
index 1983078ec2decfd405a62484f0bbaf0703426393..1c9a7da96c98a4c9ee823b3ec23dec0a675686d3 100644 (file)
@@ -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', () => {
index 76136ad67d6b27af14f262e513bdc36ac37171d3..4acaf357bd9e5cab252016ca9e2521721d41643f 100644 (file)
@@ -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)
index 7e53929904b5c8b388555a7ee73dccfe5af4c5b4..01f3889079065836e2b57016d22656eb743a6f84 100644 (file)
@@ -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>
index c7a200683853480ed53633f6eacaed13187faf2b..93bad2515b0da8ba4525120104220179802646d9 100644 (file)
@@ -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
     });
   }
 
index 5ea85d8a6697ac2c9339d79ef38af2d42c7d76b8..e2cd08698cefa11c440ae19e79f1f12fa8c43b84 100644 (file)
@@ -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>
index c366398e56efdc75997c7f297aa4840d59b2bd2d..2fd0450b1bdcbe1a3e7416cd835249962911af9e 100644 (file)
@@ -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>
index 54fa91cc49db31c4ad1dd963d03b393b932d1b72..4072c84db21e7ae7708b03c4d87ad519d14ce7cd 100644 (file)
@@ -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>
index 62866095c171e640d73581e8dd80ff1389f87721..1a879d91adfc5c5e98d1cd3f3776706ad9542427 100644 (file)
@@ -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>
index f8686659326960282c8d50d50cff72b26c2549e2..05ad9a3158092c8d6b04ac1ff6fa31fc628df864 100644 (file)
@@ -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
index 481846b23ada78494abc714df2220d0b1d077ea9..7edb44e1c23854c3d2154dd2972fbf4b57bb0a4b 100644 (file)
@@ -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>
index af4f60216e02498f10ddf755536da7e076329af4..d4b1df5a548de1a8e636df355816f9fbfda612a8 100644 (file)
@@ -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(
index 18b7dbd6f95f16a295f4cbbbcf7e47f7aedd41d3..38ebd8349890248e7789f753b93097400a721409 100644 (file)
@@ -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 {
index dee23f8152b147fd9ca06731acacb80cb4525e15..ffc8b0668b7c0244e7c19eac25cf7e1ff9842053 100644 (file)
@@ -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/components/common/FiltersHeader.tsx b/server/sonar-web/src/main/js/components/common/FiltersHeader.tsx
new file mode 100644 (file)
index 0000000..3356746
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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 { translate } from '../../helpers/l10n';
+
+interface Props {
+  displayReset: boolean;
+  onReset: () => void;
+}
+
+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="search-navigator-filters-header">
+        {this.props.displayReset && (
+          <div className="pull-right">
+            <button
+              className="button-red"
+              id="coding-rules-clear-all-filters"
+              onClick={this.handleResetClick}>
+              {translate('clear_all_filters')}
+            </button>
+          </div>
+        )}
+
+        <h3>{translate('filters')}</h3>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.js b/server/sonar-web/src/main/js/components/common/MarkdownTips.js
deleted file mode 100644 (file)
index aeb95c0..0000000
+++ /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.
- */
-// @flow
-import React from 'react';
-import { getMarkdownHelpUrl } from '../../helpers/urls';
-import { translate } from '../../helpers/l10n';
-
-export default class MarkdownTips extends React.PureComponent {
-  handleClick(evt /*: MouseEvent */) {
-    evt.preventDefault();
-    window.open(getMarkdownHelpUrl(), 'Markdown', 'height=300,width=600,scrollbars=1,resizable=1');
-  }
-
-  render() {
-    return (
-      <div className="markdown-tips">
-        <a className="little-spacer-right" href="#" onClick={this.handleClick}>
-          {translate('markdown.helplink')}
-        </a>
-        {':'}
-        <span className="spacer-left">*{translate('bold')}*</span>
-        <span className="spacer-left">``{translate('code')}``</span>
-        <span className="spacer-left">* {translate('bulleted_point')}</span>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.tsx b/server/sonar-web/src/main/js/components/common/MarkdownTips.tsx
new file mode 100644 (file)
index 0000000..9375554
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 { getMarkdownHelpUrl } from '../../helpers/urls';
+import { translate } from '../../helpers/l10n';
+
+export default class MarkdownTips extends React.PureComponent {
+  handleClick(evt: React.SyntheticEvent<HTMLAnchorElement>) {
+    evt.preventDefault();
+    window.open(getMarkdownHelpUrl(), 'Markdown', 'height=300,width=600,scrollbars=1,resizable=1');
+  }
+
+  render() {
+    return (
+      <div className="markdown-tips">
+        <a className="little-spacer-right" href="#" onClick={this.handleClick}>
+          {translate('markdown.helplink')}
+        </a>
+        {':'}
+        <span className="spacer-left">*{translate('bold')}*</span>
+        <span className="spacer-left">``{translate('code')}``</span>
+        <span className="spacer-left">* {translate('bulleted_point')}</span>
+      </div>
+    );
+  }
+}
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 (file)
index 0000000..2ef4e27
--- /dev/null
@@ -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 (file)
index 8e65eef..0000000
+++ /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 (file)
index d6e5387..0000000
+++ /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> :
-  &nbsp; *{{t 'bold'}}* &nbsp;&nbsp; ``{{t 'code'}}`` &nbsp;&nbsp; * {{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 (file)
index 0000000..134e61a
--- /dev/null
@@ -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>
+    );
+  }
+}
index d85afa7fb5e472e91eda53b49d0de98407b5fa9d..a4bb8196492c916e880d304275cfecd2d81d4082 100644 (file)
@@ -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.js
deleted file mode 100644 (file)
index bed2a3a..0000000
+++ /dev/null
@@ -1,135 +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 { debounce } from 'lodash';
-import Select from '../../components/controls/Select';
-import { translate, translateWithParameters } from '../../helpers/l10n';
-
-/*::
-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
-|};
-*/
-
-/*::
-type State = {
-  loading: boolean,
-  options: Array<Option>,
-  query: string
-};
-*/
-
-export default class SearchSelect extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static defaultProps = {
-    autofocus: true,
-    minimumQueryLength: 2,
-    resetOnBlur: true
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = { loading: false, options: [], query: '' };
-    this.search = debounce(this.search, 250);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  search = (query /*: string */) => {
-    this.props.onSearch(query).then(
-      options => {
-        if (this.mounted) {
-          this.setState({ loading: false, options });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  handleChange = (option /*: Option */) => {
-    this.props.onSelect(option.value);
-  };
-
-  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) {
-      this.setState({ loading: true, query });
-      this.search(query);
-    } else if (query.length > 0) {
-      this.setState({ options: [], query });
-    }
-  };
-
-  // disable internal filtering
-  handleFilterOption = () => true;
-
-  render() {
-    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)
-            : 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')}
-        searchable={true}
-        value={this.props.value}
-        valueRenderer={this.props.renderOption}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx b/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx
new file mode 100644 (file)
index 0000000..88b6d07
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * 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 { debounce } from 'lodash';
+import Select from '../../components/controls/Select';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+
+type Option = { label: string; 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;
+}
+
+interface State {
+  loading: boolean;
+  options: Option[];
+  query: string;
+}
+
+export default class SearchSelect extends React.PureComponent<Props, State> {
+  mounted: boolean;
+
+  static defaultProps = {
+    autofocus: true,
+    resetOnBlur: true
+  };
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { loading: false, options: [], query: '' };
+    this.search = debounce(this.search, 250);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  get minimumQueryLength() {
+    return this.props.minimumQueryLength || 2;
+  }
+
+  search = (query: string) => {
+    this.props.onSearch(query).then(
+      options => {
+        if (this.mounted) {
+          this.setState({ loading: false, options });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleChange = (option: Option) => {
+    this.props.onSelect(option.value);
+  };
+
+  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.minimumQueryLength) {
+      this.setState({ loading: true, query });
+      this.search(query);
+    } else if (query.length > 0) {
+      this.setState({ options: [], query });
+    }
+  };
+
+  // disable internal filtering
+  handleFilterOption = () => true;
+
+  render() {
+    return (
+      <Select
+        autofocus={this.props.autofocus}
+        className="input-super-large"
+        clearable={false}
+        filterOption={this.handleFilterOption}
+        isLoading={this.state.loading}
+        noResultsText={
+          this.state.query.length < this.minimumQueryLength
+            ? translateWithParameters('select2.tooShort', this.minimumQueryLength)
+            : translate('select2.noMatches')
+        }
+        onBlurResetsInput={this.props.resetOnBlur}
+        onChange={this.handleChange}
+        onInputChange={this.handleInputChange}
+        optionRenderer={this.props.renderOption}
+        options={this.state.options}
+        placeholder={translate('search_verb')}
+        searchable={true}
+        value={this.props.value}
+        valueRenderer={this.props.renderOption}
+      />
+    );
+  }
+}
index 00b34224f8c42eb10a6e0ee364f54bed9402f2b7..0ecd070f31d76b3c65e1d75d209db1abba15228a 100644 (file)
@@ -3,7 +3,6 @@
 exports[`should render Select 1`] = `
 <Select
   autofocus={true}
-  cache={false}
   className="input-super-large"
   clearable={false}
   filterOption={[Function]}
index d7d01293472b31b02ba664224c88d0cf896bf66d..bc84ab6c5122fabc6933ee043cc70c65b05c0427 100644 (file)
   white-space: nowrap;
 }
 
-.Select-value svg,
 .Select-value [class^='icon-'] {
   padding-top: 5px;
 }
 
+.Select-value svg,
 .Select-value img {
   padding-top: 3px;
 }
 }
 
 .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.js
deleted file mode 100644 (file)
index dcc8268..0000000
+++ /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.
- */
-// @flow
-import React from 'react';
-
-/*::
-type Props = {|
-  children?: React.Element<*>
-|};
-*/
-
-export default function FacetBox(props /*: Props */) {
-  return <div className="search-navigator-facet-box">{props.children}</div>;
-}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetBox.tsx b/server/sonar-web/src/main/js/components/facet/FacetBox.tsx
new file mode 100644 (file)
index 0000000..1db88a9
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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';
+
+interface Props {
+  className?: string;
+  children: React.ReactNode;
+  property: string;
+}
+
+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/components/facet/FacetFooter.js b/server/sonar-web/src/main/js/components/facet/FacetFooter.js
deleted file mode 100644 (file)
index 6e67ec9..0000000
+++ /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.
- */
-// @flow
-import React from 'react';
-import SearchSelect from '../controls/SearchSelect';
-
-/*::
-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; */
-
-  render() {
-    return (
-      <div className="search-navigator-facet-footer">
-        <SearchSelect autofocus={false} {...this.props} />
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx b/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx
new file mode 100644 (file)
index 0000000..2f34ccf
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 SearchSelect from '../controls/SearchSelect';
+
+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.js
deleted file mode 100644 (file)
index 8bda5f2..0000000
+++ /dev/null
@@ -1,123 +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 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
-  };
-
-  handleClearClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    if (this.props.onClear) {
-      this.props.onClear();
-    }
-  };
-
-  handleClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    if (this.props.onClick) {
-      this.props.onClick();
-    }
-  };
-
-  renderHelper() {
-    if (!this.props.helper) {
-      return null;
-    }
-    return (
-      <Tooltip overlay={this.props.helper} placement="right">
-        <span>
-          <HelpIcon className="spacer-left text-info" />
-        </span>
-      </Tooltip>
-    );
-  }
-
-  renderValueIndicator() {
-    const { values } = this.props;
-    if (this.props.open || !values || !values.length) {
-      return null;
-    }
-    const value =
-      values.length === 1 ? values[0] : translateWithParameters('x_selected', values.length);
-    return (
-      <span className="badge badge-secondary is-rounded text-ellipsis" title={value}>
-        {value}
-      </span>
-    );
-  }
-
-  render() {
-    const showClearButton =
-      this.props.values != null && this.props.values.length > 0 && this.props.onClear != null;
-
-    return (
-      <div className="search-navigator-facet-header-wrapper">
-        {this.props.onClick ? (
-          <span className="search-navigator-facet-header">
-            <a href="#" onClick={this.handleClick}>
-              <OpenCloseIcon className="little-spacer-right" open={this.props.open} />
-              {this.props.name}
-            </a>
-            {this.renderHelper()}
-          </span>
-        ) : (
-          <span className="search-navigator-facet-header">
-            {this.props.name}
-            {this.renderHelper()}
-          </span>
-        )}
-
-        <span className="search-navigator-facet-header-value spacer-left spacer-right ">
-          {this.renderValueIndicator()}
-        </span>
-
-        {showClearButton && (
-          <button
-            className="search-navigator-facet-header-button button-small button-red"
-            onClick={this.handleClearClick}>
-            {translate('clear')}
-          </button>
-        )}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx b/server/sonar-web/src/main/js/components/facet/FacetHeader.tsx
new file mode 100644 (file)
index 0000000..cfe3f06
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * 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 OpenCloseIcon from '../icons-components/OpenCloseIcon';
+import HelpIcon from '../icons-components/HelpIcon';
+import Tooltip from '../controls/Tooltip';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+
+interface Props {
+  helper?: string;
+  name: string;
+  onClear?: () => void;
+  onClick?: () => void;
+  open: boolean;
+  values?: string[];
+}
+
+export default class FacetHeader extends React.PureComponent<Props> {
+  handleClearClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    if (this.props.onClear) {
+      this.props.onClear();
+    }
+  };
+
+  handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    if (this.props.onClick) {
+      this.props.onClick();
+    }
+  };
+
+  renderHelper() {
+    if (!this.props.helper) {
+      return null;
+    }
+    return (
+      <Tooltip overlay={this.props.helper} placement="right">
+        <span>
+          <HelpIcon className="spacer-left text-info" />
+        </span>
+      </Tooltip>
+    );
+  }
+
+  renderValueIndicator() {
+    const { values } = this.props;
+    if (this.props.open || !values || !values.length) {
+      return null;
+    }
+    const value =
+      values.length === 1 ? values[0] : translateWithParameters('x_selected', values.length);
+    return (
+      <span className="badge badge-secondary is-rounded text-ellipsis" title={value}>
+        {value}
+      </span>
+    );
+  }
+
+  render() {
+    const showClearButton =
+      this.props.values != null && this.props.values.length > 0 && this.props.onClear != null;
+
+    return (
+      <div className="search-navigator-facet-header-wrapper">
+        {this.props.onClick ? (
+          <span className="search-navigator-facet-header">
+            <a href="#" onClick={this.handleClick}>
+              <OpenCloseIcon className="little-spacer-right" open={this.props.open} />
+              {this.props.name}
+            </a>
+            {this.renderHelper()}
+          </span>
+        ) : (
+          <span className="search-navigator-facet-header">
+            {this.props.name}
+            {this.renderHelper()}
+          </span>
+        )}
+
+        <span className="search-navigator-facet-header-value spacer-left spacer-right ">
+          {this.renderValueIndicator()}
+        </span>
+
+        {showClearButton && (
+          <button
+            className="search-navigator-facet-header-button button-small button-red"
+            onClick={this.handleClearClick}>
+            {translate('clear')}
+          </button>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.js b/server/sonar-web/src/main/js/components/facet/FacetItem.js
deleted file mode 100644 (file)
index e4d97da..0000000
+++ /dev/null
@@ -1,67 +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 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; */
-
-  static defaultProps = {
-    disabled: false,
-    halfWidth: false
-  };
-
-  handleClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
-    event.preventDefault();
-    this.props.onClick(this.props.value);
-  };
-
-  render() {
-    const className = classNames('facet', 'search-navigator-facet', {
-      active: this.props.active,
-      'search-navigator-facet-half': this.props.halfWidth
-    });
-
-    return this.props.disabled ? (
-      <span className={className}>
-        <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}>
-        <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/FacetItem.tsx b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
new file mode 100644 (file)
index 0000000..a960cb8
--- /dev/null
@@ -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 * as classNames from 'classnames';
+
+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: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onClick(this.props.value);
+  };
+
+  render() {
+    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} 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} 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.js
deleted file mode 100644 (file)
index da8d2c8..0000000
+++ /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.
- */
-// @flow
-import React from 'react';
-
-/*::
-type Props = {|
-  children?: Array<React.Element<*>>
-|};
-*/
-
-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/FacetItemsList.tsx b/server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx
new file mode 100644 (file)
index 0000000..3fa2bf5
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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';
+
+interface Props {
+  children?: React.ReactNode;
+}
+
+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.js
deleted file mode 100644 (file)
index 005ecce..0000000
+++ /dev/null
@@ -1,33 +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 { shallow } from 'enzyme';
-import FacetBox from '../FacetBox';
-
-it('should render', () => {
-  expect(
-    shallow(
-      <FacetBox>
-        <div />
-      </FacetBox>
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetBox-test.tsx
new file mode 100644 (file)
index 0000000..3595b34
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import FacetBox from '../FacetBox';
+
+it('should render', () => {
+  expect(
+    shallow(
+      <FacetBox property="foo">
+        <div />
+      </FacetBox>
+    )
+  ).toMatchSnapshot();
+});
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.js
deleted file mode 100644 (file)
index c287824..0000000
+++ /dev/null
@@ -1,27 +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 { shallow } from 'enzyme';
-import FacetFooter from '../FacetFooter';
-
-it('should render', () => {
-  expect(shallow(<FacetFooter onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.tsx
new file mode 100644 (file)
index 0000000..ce1f6f4
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import FacetFooter from '../FacetFooter';
+
+it('should render', () => {
+  expect(shallow(<FacetFooter onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
+});
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.js
deleted file mode 100644 (file)
index 7249c7c..0000000
+++ /dev/null
@@ -1,71 +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 { shallow } from 'enzyme';
-import { click } from '../../../helpers/testUtils';
-import FacetHeader from '../FacetHeader';
-
-it('should render open facet with value', () => {
-  expect(
-    shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} values={['foo']} />)
-  ).toMatchSnapshot();
-});
-
-it('should render open facet without value', () => {
-  expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} />)).toMatchSnapshot();
-});
-
-it('should render closed facet with value', () => {
-  expect(
-    shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} values={['foo']} />)
-  ).toMatchSnapshot();
-});
-
-it('should render closed facet without value', () => {
-  expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} />)).toMatchSnapshot();
-});
-
-it('should render without link', () => {
-  expect(shallow(<FacetHeader name="foo" open={false} />)).toMatchSnapshot();
-});
-
-it('should call onClick', () => {
-  const onClick = jest.fn();
-  const wrapper = shallow(<FacetHeader name="foo" onClick={onClick} open={false} />);
-  click(wrapper.find('a'));
-  expect(onClick).toHaveBeenCalled();
-});
-
-it('should clear', () => {
-  const onClear = jest.fn();
-  const wrapper = shallow(
-    <FacetHeader
-      name="foo"
-      onClear={onClear}
-      onClick={jest.fn()}
-      open={false}
-      values={['foo', 'bar', 'baz']}
-    />
-  );
-  expect(wrapper).toMatchSnapshot();
-  click(wrapper.find('.button-red'));
-  expect(onClear).toHaveBeenCalled();
-});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetHeader-test.tsx
new file mode 100644 (file)
index 0000000..3a292e5
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import FacetHeader from '../FacetHeader';
+import { click } from '../../../helpers/testUtils';
+
+it('should render open facet with value', () => {
+  expect(
+    shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} values={['foo']} />)
+  ).toMatchSnapshot();
+});
+
+it('should render open facet without value', () => {
+  expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} />)).toMatchSnapshot();
+});
+
+it('should render closed facet with value', () => {
+  expect(
+    shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} values={['foo']} />)
+  ).toMatchSnapshot();
+});
+
+it('should render closed facet without value', () => {
+  expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} />)).toMatchSnapshot();
+});
+
+it('should render without link', () => {
+  expect(shallow(<FacetHeader name="foo" open={false} />)).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+  const onClick = jest.fn();
+  const wrapper = shallow(<FacetHeader name="foo" onClick={onClick} open={false} />);
+  click(wrapper.find('a'));
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('should clear', () => {
+  const onClear = jest.fn();
+  const wrapper = shallow(
+    <FacetHeader
+      name="foo"
+      onClear={onClear}
+      onClick={jest.fn()}
+      open={false}
+      values={['foo', 'bar', 'baz']}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('.button-red'));
+  expect(onClear).toHaveBeenCalled();
+});
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.js
deleted file mode 100644 (file)
index b3cd993..0000000
+++ /dev/null
@@ -1,60 +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 { 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} />
-  );
-
-it('should render active', () => {
-  expect(renderFacetItem({ active: true })).toMatchSnapshot();
-});
-
-it('should render inactive', () => {
-  expect(renderFacetItem({ active: false })).toMatchSnapshot();
-});
-
-it('should render stat', () => {
-  expect(renderFacetItem({ stat: '13' })).toMatchSnapshot();
-});
-
-it('should render disabled', () => {
-  expect(renderFacetItem({ disabled: true })).toMatchSnapshot();
-});
-
-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' } } });
-  expect(onClick).toHaveBeenCalled();
-});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx
new file mode 100644 (file)
index 0000000..e16c502
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import { click } from '../../../helpers/testUtils';
+import FacetItem, { Props } from '../FacetItem';
+
+it('should render active', () => {
+  expect(renderFacetItem({ active: true })).toMatchSnapshot();
+});
+
+it('should render inactive', () => {
+  expect(renderFacetItem({ active: false })).toMatchSnapshot();
+});
+
+it('should render stat', () => {
+  expect(renderFacetItem({ stat: '13' })).toMatchSnapshot();
+});
+
+it('should render disabled', () => {
+  expect(renderFacetItem({ disabled: true })).toMatchSnapshot();
+});
+
+it('should render half width', () => {
+  expect(renderFacetItem({ halfWidth: true })).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+  const onClick = jest.fn();
+  const wrapper = renderFacetItem({ onClick });
+  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.js
deleted file mode 100644 (file)
index ad6b3a5..0000000
+++ /dev/null
@@ -1,33 +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 { shallow } from 'enzyme';
-import FacetItemsList from '../FacetItemsList';
-
-it('should render', () => {
-  expect(
-    shallow(
-      <FacetItemsList>
-        <div />
-      </FacetItemsList>
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItemsList-test.tsx
new file mode 100644 (file)
index 0000000..5bd2034
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import FacetItemsList from '../FacetItemsList';
+
+it('should render', () => {
+  expect(
+    shallow(
+      <FacetItemsList>
+        <div />
+      </FacetItemsList>
+    )
+  ).toMatchSnapshot();
+});
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.js.snap
deleted file mode 100644 (file)
index e28d453..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
-  className="search-navigator-facet-box"
->
-  <div />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetBox-test.tsx.snap
new file mode 100644 (file)
index 0000000..3173290
--- /dev/null
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+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.js.snap
deleted file mode 100644 (file)
index e57d0a0..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
-  className="search-navigator-facet-footer"
->
-  <SearchSelect
-    autofocus={false}
-    minimumQueryLength={2}
-    onSearch={[MockFunction]}
-    onSelect={[MockFunction]}
-    resetOnBlur={true}
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.tsx.snap
new file mode 100644 (file)
index 0000000..3fab99c
--- /dev/null
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+  className="search-navigator-facet-footer"
+>
+  <SearchSelect
+    autofocus={false}
+    onSearch={[MockFunction]}
+    onSelect={[MockFunction]}
+    resetOnBlur={true}
+  />
+</div>
+`;
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.js.snap
deleted file mode 100644 (file)
index dad6166..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should clear 1`] = `
-<div
-  className="search-navigator-facet-header-wrapper"
->
-  <span
-    className="search-navigator-facet-header"
-  >
-    <a
-      href="#"
-      onClick={[Function]}
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={false}
-      />
-      foo
-    </a>
-  </span>
-  <span
-    className="search-navigator-facet-header-value spacer-left spacer-right "
-  >
-    <span
-      className="badge badge-secondary is-rounded text-ellipsis"
-      title="x_selected.3"
-    >
-      x_selected.3
-    </span>
-  </span>
-  <button
-    className="search-navigator-facet-header-button button-small button-red"
-    onClick={[Function]}
-  >
-    clear
-  </button>
-</div>
-`;
-
-exports[`should render closed facet with value 1`] = `
-<div
-  className="search-navigator-facet-header-wrapper"
->
-  <span
-    className="search-navigator-facet-header"
-  >
-    <a
-      href="#"
-      onClick={[Function]}
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={false}
-      />
-      foo
-    </a>
-  </span>
-  <span
-    className="search-navigator-facet-header-value spacer-left spacer-right "
-  >
-    <span
-      className="badge badge-secondary is-rounded text-ellipsis"
-      title="foo"
-    >
-      foo
-    </span>
-  </span>
-</div>
-`;
-
-exports[`should render closed facet without value 1`] = `
-<div
-  className="search-navigator-facet-header-wrapper"
->
-  <span
-    className="search-navigator-facet-header"
-  >
-    <a
-      href="#"
-      onClick={[Function]}
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={false}
-      />
-      foo
-    </a>
-  </span>
-  <span
-    className="search-navigator-facet-header-value spacer-left spacer-right "
-  />
-</div>
-`;
-
-exports[`should render open facet with value 1`] = `
-<div
-  className="search-navigator-facet-header-wrapper"
->
-  <span
-    className="search-navigator-facet-header"
-  >
-    <a
-      href="#"
-      onClick={[Function]}
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={true}
-      />
-      foo
-    </a>
-  </span>
-  <span
-    className="search-navigator-facet-header-value spacer-left spacer-right "
-  />
-</div>
-`;
-
-exports[`should render open facet without value 1`] = `
-<div
-  className="search-navigator-facet-header-wrapper"
->
-  <span
-    className="search-navigator-facet-header"
-  >
-    <a
-      href="#"
-      onClick={[Function]}
-    >
-      <OpenCloseIcon
-        className="little-spacer-right"
-        open={true}
-      />
-      foo
-    </a>
-  </span>
-  <span
-    className="search-navigator-facet-header-value spacer-left spacer-right "
-  />
-</div>
-`;
-
-exports[`should render without link 1`] = `
-<div
-  className="search-navigator-facet-header-wrapper"
->
-  <span
-    className="search-navigator-facet-header"
-  >
-    foo
-  </span>
-  <span
-    className="search-navigator-facet-header-value spacer-left spacer-right "
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetHeader-test.tsx.snap
new file mode 100644 (file)
index 0000000..dad6166
--- /dev/null
@@ -0,0 +1,156 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should clear 1`] = `
+<div
+  className="search-navigator-facet-header-wrapper"
+>
+  <span
+    className="search-navigator-facet-header"
+  >
+    <a
+      href="#"
+      onClick={[Function]}
+    >
+      <OpenCloseIcon
+        className="little-spacer-right"
+        open={false}
+      />
+      foo
+    </a>
+  </span>
+  <span
+    className="search-navigator-facet-header-value spacer-left spacer-right "
+  >
+    <span
+      className="badge badge-secondary is-rounded text-ellipsis"
+      title="x_selected.3"
+    >
+      x_selected.3
+    </span>
+  </span>
+  <button
+    className="search-navigator-facet-header-button button-small button-red"
+    onClick={[Function]}
+  >
+    clear
+  </button>
+</div>
+`;
+
+exports[`should render closed facet with value 1`] = `
+<div
+  className="search-navigator-facet-header-wrapper"
+>
+  <span
+    className="search-navigator-facet-header"
+  >
+    <a
+      href="#"
+      onClick={[Function]}
+    >
+      <OpenCloseIcon
+        className="little-spacer-right"
+        open={false}
+      />
+      foo
+    </a>
+  </span>
+  <span
+    className="search-navigator-facet-header-value spacer-left spacer-right "
+  >
+    <span
+      className="badge badge-secondary is-rounded text-ellipsis"
+      title="foo"
+    >
+      foo
+    </span>
+  </span>
+</div>
+`;
+
+exports[`should render closed facet without value 1`] = `
+<div
+  className="search-navigator-facet-header-wrapper"
+>
+  <span
+    className="search-navigator-facet-header"
+  >
+    <a
+      href="#"
+      onClick={[Function]}
+    >
+      <OpenCloseIcon
+        className="little-spacer-right"
+        open={false}
+      />
+      foo
+    </a>
+  </span>
+  <span
+    className="search-navigator-facet-header-value spacer-left spacer-right "
+  />
+</div>
+`;
+
+exports[`should render open facet with value 1`] = `
+<div
+  className="search-navigator-facet-header-wrapper"
+>
+  <span
+    className="search-navigator-facet-header"
+  >
+    <a
+      href="#"
+      onClick={[Function]}
+    >
+      <OpenCloseIcon
+        className="little-spacer-right"
+        open={true}
+      />
+      foo
+    </a>
+  </span>
+  <span
+    className="search-navigator-facet-header-value spacer-left spacer-right "
+  />
+</div>
+`;
+
+exports[`should render open facet without value 1`] = `
+<div
+  className="search-navigator-facet-header-wrapper"
+>
+  <span
+    className="search-navigator-facet-header"
+  >
+    <a
+      href="#"
+      onClick={[Function]}
+    >
+      <OpenCloseIcon
+        className="little-spacer-right"
+        open={true}
+      />
+      foo
+    </a>
+  </span>
+  <span
+    className="search-navigator-facet-header-value spacer-left spacer-right "
+  />
+</div>
+`;
+
+exports[`should render without link 1`] = `
+<div
+  className="search-navigator-facet-header-wrapper"
+>
+  <span
+    className="search-navigator-facet-header"
+  >
+    foo
+  </span>
+  <span
+    className="search-navigator-facet-header-value spacer-left spacer-right "
+  />
+</div>
+`;
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.js.snap
deleted file mode 100644 (file)
index 6aff532..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render active 1`] = `
-<a
-  className="facet search-navigator-facet active"
-  href="#"
-  onClick={[Function]}
->
-  <span
-    className="facet-name"
-  >
-    foo
-  </span>
-</a>
-`;
-
-exports[`should render disabled 1`] = `
-<span
-  className="facet search-navigator-facet"
->
-  <span
-    className="facet-name"
-  >
-    foo
-  </span>
-</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"
-  href="#"
-  onClick={[Function]}
->
-  <span
-    className="facet-name"
-  >
-    foo
-  </span>
-</a>
-`;
-
-exports[`should render inactive 1`] = `
-<a
-  className="facet search-navigator-facet"
-  href="#"
-  onClick={[Function]}
->
-  <span
-    className="facet-name"
-  >
-    foo
-  </span>
-</a>
-`;
-
-exports[`should render stat 1`] = `
-<a
-  className="facet search-navigator-facet"
-  href="#"
-  onClick={[Function]}
->
-  <span
-    className="facet-name"
-  >
-    foo
-  </span>
-  <span
-    className="facet-stat"
-  >
-    13
-  </span>
-</a>
-`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap
new file mode 100644 (file)
index 0000000..c0cf807
--- /dev/null
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render active 1`] = `
+<a
+  className="facet search-navigator-facet active"
+  data-facet="bar"
+  href="#"
+  onClick={[Function]}
+>
+  <span
+    className="facet-name"
+  >
+    foo
+  </span>
+</a>
+`;
+
+exports[`should render disabled 1`] = `
+<span
+  className="facet search-navigator-facet"
+  data-facet="bar"
+>
+  <span
+    className="facet-name"
+  >
+    foo
+  </span>
+</span>
+`;
+
+exports[`should render half width 1`] = `
+<a
+  className="facet search-navigator-facet search-navigator-facet-half"
+  data-facet="bar"
+  href="#"
+  onClick={[Function]}
+>
+  <span
+    className="facet-name"
+  >
+    foo
+  </span>
+</a>
+`;
+
+exports[`should render inactive 1`] = `
+<a
+  className="facet search-navigator-facet"
+  data-facet="bar"
+  href="#"
+  onClick={[Function]}
+>
+  <span
+    className="facet-name"
+  >
+    foo
+  </span>
+</a>
+`;
+
+exports[`should render stat 1`] = `
+<a
+  className="facet search-navigator-facet"
+  data-facet="bar"
+  href="#"
+  onClick={[Function]}
+>
+  <span
+    className="facet-name"
+  >
+    foo
+  </span>
+  <span
+    className="facet-stat"
+  >
+    13
+  </span>
+</a>
+`;
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.js.snap
deleted file mode 100644 (file)
index 9962cfc..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
-  className="search-navigator-facet-list"
->
-  <div />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItemsList-test.tsx.snap
new file mode 100644 (file)
index 0000000..9962cfc
--- /dev/null
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+  className="search-navigator-facet-list"
+>
+  <div />
+</div>
+`;
index ee6a6e08e258505b9fd1f407ed0f80549cd081c8..e4ba9ab996805ade0df8451c0d75d61855515637 100644 (file)
@@ -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')]}
         />
       );
     }
index 055531ca38d38c62188e72ff8280dda34efd03f5..178f8b75e19613b33674275289b7d4ddbc8bcbbe 100644 (file)
@@ -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 (file)
index 6b3f00c..0000000
+++ /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 (file)
index ca64a30..0000000
+++ /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 (file)
index 4cf6963..0000000
+++ /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 (file)
index ce1e620..0000000
+++ /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 (file)
index 513474c..0000000
+++ /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 (file)
index 6004255..0000000
+++ /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 (file)
index 2ff317a..0000000
+++ /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 (file)
index ea09144..0000000
+++ /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 (file)
index 9c36540..0000000
+++ /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 (file)
index 6f262a3..0000000
+++ /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.js
deleted file mode 100644 (file)
index 217603e..0000000
+++ /dev/null
@@ -1,36 +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 SeverityIcon from './SeverityIcon';
-import { translate } from '../../helpers/l10n';
-
-export default function SeverityHelper(props /*: { severity: ?string, className?: string } */) {
-  const { severity } = props;
-  if (!severity) {
-    return null;
-  }
-  return (
-    <span className={props.className}>
-      <SeverityIcon className="little-spacer-right" severity={severity} />
-      {translate('severity', severity)}
-    </span>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/shared/SeverityHelper.tsx b/server/sonar-web/src/main/js/components/shared/SeverityHelper.tsx
new file mode 100644 (file)
index 0000000..4078842
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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 SeverityIcon from './SeverityIcon';
+import { translate } from '../../helpers/l10n';
+
+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={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.js
deleted file mode 100644 (file)
index ada0785..0000000
+++ /dev/null
@@ -1,38 +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 IssueTypeIcon from '../ui/IssueTypeIcon';
-import { translate } from '../../helpers/l10n';
-
-/*::
-type Props = {
-  type: string
-};
-*/
-
-const TypeHelper = (props /*: Props */) => (
-  <span>
-    <IssueTypeIcon className="little-spacer-right" query={props.type} />
-    {translate('issue.type', props.type)}
-  </span>
-);
-
-export default TypeHelper;
diff --git a/server/sonar-web/src/main/js/components/shared/TypeHelper.tsx b/server/sonar-web/src/main/js/components/shared/TypeHelper.tsx
new file mode 100644 (file)
index 0000000..1229bf3
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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 IssueTypeIcon from '../ui/IssueTypeIcon';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+  className?: string;
+  type: string;
+}
+
+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>
+  );
+}
index a1fc9a39488b3cff53d0caffe45dafc5cc54d788..9b7931c4722c818fded04f9a1870e21d72b3aa29 100644 (file)
@@ -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>
   );
index 2d3d8b71ef560edc4b1da1721f6cbabadb323454..7b75a3aa5cf64ccc4110b7b905595c2a78b9e4e6 100644 (file)
@@ -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 (file)
index 0000000..d430585
--- /dev/null
@@ -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>
+`;
index 5ae7b8c1679ff4f412f8a70b15188392cc89a04d..88c2dbb5b8f45a5d7ec78db0951cae27c821b23a 100644 (file)
@@ -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),
index 519b0473e1c873791e2f12f392bcf88d54a6a05f..7c6a893ca7f1b4944486ed01ca4ef2d92ec8c065 100644 (file)
@@ -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/avatarHelper.js b/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js
deleted file mode 100644 (file)
index f67f8a8..0000000
+++ /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.
- */
-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;
-
-module.exports = function(hash, name, size) {
-  return new Handlebars.default.SafeString(
-    renderToString(
-      <WithStore>
-        <Avatar hash={hash} name={name} size={size} />
-      </WithStore>
-    )
-  );
-};
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 (file)
index 93e602e..0000000
+++ /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 (file)
index bdca5f2..0000000
+++ /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 (file)
index 7d054ee..0000000
+++ /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 (file)
index 4fea69b..0000000
+++ /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;
-};
index 1ed0b7913172198dec6365edd14583592eaab9f1..e495eb7da3a8fa578d6f48b12506b0b0e2fe79d4 100644 (file)
@@ -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;
+  }
+}
index d698665e09073f5413847b0265395ed9dde1dbcc..86367ed4f2c97872bc7e211c53490b88153fd038 100644 (file)
@@ -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;
index 51666283ff8ca2531815d81ee06a5d19f48518d1..732c0b6a6bebc8e7e73a738ebed4ff3df3a72cb3 100644 (file)
@@ -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';
 }
index 43f6f31693e3130ae2fdbc8d9bb0afb7964e68d4..5c33dbc9e4756f1b3d3076c1f9691511da1114b0 100644 (file)
   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"
index a02889ca61468134450cd55a0791a5ed058dbb16..5a2a3cf89817941c0090c8b9c2fc8773b46d7af2 100644 (file)
@@ -1196,6 +1196,7 @@ coding_rules.active_in_all_profiles=The rule is already activated on all availab
 coding_rules.activate=Activate
 coding_rules.activate_in=Activate In
 coding_rules.activate_in_quality_profile=Activate In Quality Profile
+coding_rules.activation_severity=Activation Severity
 coding_rules.available_since=Available Since
 coding_rules.bulk_change=Bulk Change
 coding_rules.bulk_change.success={2} rule(s) changed in profile {0} - {1}
@@ -1232,11 +1233,13 @@ coding_rules.quality_profile=Quality Profile
 coding_rules.reactivate=Reactivate
 coding_rules.reactivate.help=A rule with the same key has been previously deleted. Please reactivate the existing rule or modify the key to create a new rule.
 coding_rules.return_to_list=Return to List
+coding_rules.remove_extended_description=Remove Extended Description
 coding_rules.remove_extended_description.confirm=Are you sure you want to remove the extended description?
+coding_rules.repository_language=Rule repository (language)
 coding_rules.revert_to_parent_definition=Revert to Parent Definition
 coding_rules.revert_to_parent_definition.confirm=This rule will be reverted to the parameters defined in profile {0}. Are you sure?
 coding_rules.rule_template=Rule Template
-coding_rules.rule_template.title=This rule can be used as a template to create custom rules,\nit cannot be activated on a profile
+coding_rules.rule_template.title=This rule can be used as a template to create custom rules, it cannot be activated on a profile
 coding_rules._rules=rules
 coding_rules.show_template=Show Template
 coding_rules.to_select_rules=to select rules
@@ -1281,8 +1284,8 @@ coding_rules.facet.severities=Default Severity
 coding_rules.facet.statuses=Status
 coding_rules.facet.available_since=Available Since
 coding_rules.facet.inheritance=Inheritance
-coding_rules.facet.active_severities=Activation Severity
-coding_rules.facet.is_template=Template
+coding_rules.facet.activationSeverities=Activation Severity
+coding_rules.facet.template=Template
 coding_rules.facet.rule_key=Rule
 coding_rules.facet.types=Type
 
index d7257e5ee142c5ca20ccacc13c6c5184ab31c1db..27db0db9d0dcce4526e35db21769e8df2499317d 100644 (file)
@@ -33,6 +33,7 @@ import org.sonarqube.tests.issue.OrganizationIssueAssignTest;
 import org.sonarqube.tests.issue.OrganizationIssuesPageTest;
 import org.sonarqube.tests.qualityProfile.BuiltInQualityProfilesTest;
 import org.sonarqube.tests.qualityProfile.CustomQualityProfilesTest;
+import org.sonarqube.tests.qualityProfile.OrganizationQualityProfilesUiTest;
 import org.sonarqube.tests.qualityProfile.QualityProfilesEditTest;
 import org.sonarqube.tests.qualityProfile.QualityProfilesWsTest;
 import org.sonarqube.tests.rule.RulesMarkdownTest;
@@ -54,6 +55,7 @@ import static util.ItUtils.xooPlugin;
   OrganizationIdentityProviderTest.class,
   OrganizationIssueAssignTest.class,
   OrganizationIssuesPageTest.class,
+  OrganizationQualityProfilesUiTest.class,
   BuiltInQualityProfilesTest.class,
   QualityProfilesEditTest.class,
   QualityProfilesWsTest.class,
index 6a42d4d8f137b37e21da750cc2ca50ce1f8e3f42..4a39d3f2aa2e41f8edfa7c177c35f985e0be4f0d 100644 (file)
@@ -170,7 +170,7 @@ public class OrganizationQualityProfilesUiTest {
     qpPage.shouldHaveMissingSonarWayRules(2);
     RulesPage rPage = qpPage.showMissingSonarWayRules();
     rPage.shouldHaveTotalRules(2);
-    rPage.getSelectedFacetItems("qprofile")
+    rPage.openFacet("profile").getSelectedFacetItems("profile")
       .shouldHaveSize(2)
       .findBy(Condition.cssClass("compare")).has(Condition.text("Sonar way"));
   }
index b35258ad768bff71868e2243b998802fd6a9d0c2..c7fc21f5ccc37812379d66b4813430ff5480c3e1 100644 (file)
@@ -24,20 +24,345 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonarqube.qa.util.Tester;
+import org.sonarqube.qa.util.pageobjects.RuleDetails;
 import org.sonarqube.qa.util.pageobjects.RulesPage;
 import org.sonarqube.tests.Category4Suite;
+import org.sonarqube.ws.Qualityprofiles.CreateWsResponse.QualityProfile;
+import org.sonarqube.ws.client.qualityprofiles.ChangeParentRequest;
+import org.sonarqube.ws.client.qualityprofiles.CreateRequest;
+import util.ProjectAnalysisRule;
+
+import static com.codeborne.selenide.WebDriverRunner.url;
+import static org.assertj.core.api.Assertions.assertThat;
 
 public class RulesPageTest {
+  private static final String SAMPLE_RULE = "xoo:HasTag";
+  private static final String SAMPLE_RULE_XOO2 = "xoo2:HasTag";
+  private static final String SAMPLE_RULE2 = "common-xoo:InsufficientBranchCoverage";
+  private static final String SAMPLE_SECURITY_RULE = "xoo:OneVulnerabilityIssuePerModule";
+  private static final String SAMPLE_TEMPLATE_RULE = "xoo:xoo-template";
+  private static final String XOO_LANG = "xoo";
+  private static int profileIndex = 0;
+
   @ClassRule
   public static Orchestrator ORCHESTRATOR = Category4Suite.ORCHESTRATOR;
 
   @Rule
   public Tester tester = new Tester(ORCHESTRATOR).disableOrganizations();
 
+  @Rule
+  public ProjectAnalysisRule projectAnalysisRule = ProjectAnalysisRule.from(ORCHESTRATOR);
+
+  @Test
+  public void should_search_rules() {
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldDisplayRules(SAMPLE_RULE, SAMPLE_RULE2);
+    page.search("branches");
+    page.shouldNotDisplayRules(SAMPLE_RULE).shouldDisplayRules(SAMPLE_RULE2);
+    assertThat(url()).contains("q=branches");
+  }
+
+  @Test
+  public void should_filter_rules_by_language() {
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldDisplayRules(SAMPLE_RULE, SAMPLE_RULE_XOO2);
+    page.selectFacetItem("languages", XOO_LANG);
+    page.shouldNotDisplayRules(SAMPLE_RULE_XOO2).shouldDisplayRules(SAMPLE_RULE);
+    assertThat(url()).contains("languages=" + XOO_LANG);
+  }
+
+  @Test
+  public void should_filter_rules_by_type() {
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldDisplayRules(SAMPLE_RULE, SAMPLE_SECURITY_RULE);
+    page.selectFacetItem("types", "VULNERABILITY");
+    page.shouldNotDisplayRules(SAMPLE_RULE).shouldDisplayRules(SAMPLE_SECURITY_RULE);
+    assertThat(url()).contains("types=VULNERABILITY");
+  }
+
+  @Test
+  public void should_filter_rules_by_default_severity() {
+    String ruleKey = "custom_blocker_rule";
+    String blockerRule = "xoo:" + ruleKey;
+    createBlockerCustomRule(ruleKey);
+
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldDisplayRules(SAMPLE_RULE, blockerRule);
+    page.openFacet("severities").selectFacetItem("severities", "BLOCKER");
+    page.shouldNotDisplayRules(SAMPLE_RULE).shouldDisplayRules(blockerRule);
+    assertThat(url()).contains("severities=BLOCKER");
+
+    deleteCustomRule(blockerRule);
+  }
+
+  @Test
+  public void should_filter_rules_by_availability_date() {
+    // TODO
+    // all rules have the same "available since" date, because this is the date when server started
+  }
+
+  @Test
+  public void should_filter_rules_by_template() {
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldDisplayRules(SAMPLE_RULE, SAMPLE_TEMPLATE_RULE);
+
+    page.openFacet("template").selectFacetItem("template", "true");
+    page.shouldNotDisplayRules(SAMPLE_RULE).shouldDisplayRules(SAMPLE_TEMPLATE_RULE);
+    assertThat(url()).contains("template=true");
+
+    page.selectFacetItem("template", "false");
+    page.shouldNotDisplayRules(SAMPLE_TEMPLATE_RULE).shouldDisplayRules(SAMPLE_RULE);
+    assertThat(url()).contains("template=false");
+  }
+
+  @Test
+  public void should_filter_rules_by_quality_profile() {
+    QualityProfile empty = createQualityProfile();
+    QualityProfile profile = createQualityProfile();
+    tester.qProfiles().activateRule(profile, SAMPLE_RULE);
+
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldDisplayRules(SAMPLE_RULE);
+
+    page.openFacet("profile").selectFacetItem("profile", empty.getKey());
+    page.shouldNotDisplayRules(SAMPLE_RULE);
+    assertThat(url()).contains("qprofile="); // TODO we don't know profile key
+
+    page.selectFacetItem("profile", profile.getKey());
+    page.shouldDisplayRules(SAMPLE_RULE);
+    assertThat(url()).contains("qprofile=" + profile.getKey());
+  }
+
+  @Test
+  public void should_filter_rules_by_inheritance() {
+    QualityProfile profile = createInheritedQualityProfile();
+
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldHaveDisabledFacet("inheritance").shouldDisplayRules(SAMPLE_RULE);
+
+    page.openFacet("profile").selectFacetItem("profile", profile.getKey());
+    page.shouldNotHaveDisabledFacet("inheritance").openFacet("inheritance");
+
+    page.selectFacetItem("inheritance", "NONE");
+    page.shouldDisplayRules(SAMPLE_RULE2).shouldNotDisplayRules(SAMPLE_RULE);
+    assertThat(url()).contains("inheritance=NONE");
+
+    page.selectFacetItem("inheritance", "INHERITED");
+    page.shouldDisplayRules("xoo:OneIssuePerLine").shouldNotDisplayRules(SAMPLE_RULE);
+    assertThat(url()).contains("inheritance=INHERITED");
+
+    page.selectFacetItem("inheritance", "OVERRIDES");
+    page.shouldDisplayRules(SAMPLE_RULE).shouldNotDisplayRules(SAMPLE_RULE2);
+    assertThat(url()).contains("inheritance=OVERRIDES");
+  }
+
+  @Test
+  public void should_filter_rules_by_activation_severity() {
+    QualityProfile profile = createQualityProfile();
+    tester.qProfiles().activateRule(profile, SAMPLE_RULE, "BLOCKER");
+
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldHaveDisabledFacet("activationSeverities").shouldDisplayRules(SAMPLE_RULE, SAMPLE_RULE2);
+
+    page.openFacet("profile").selectFacetItem("profile", profile.getKey());
+    page.shouldNotHaveDisabledFacet("activationSeverities").openFacet("activationSeverities");
+
+    page.selectFacetItem("activationSeverities", "BLOCKER");
+    page.shouldDisplayRules(SAMPLE_RULE).shouldNotDisplayRules(SAMPLE_RULE2);
+    assertThat(url()).contains("active_severities=BLOCKER");
+  }
+
+  @Test
+  public void should_clear_all_filters() {
+    RulesPage page = tester.openBrowser().openRules()
+      .shouldDisplayRules(SAMPLE_RULE, SAMPLE_RULE_XOO2, SAMPLE_RULE2);
+
+    page.search("branches")
+      .shouldNotDisplayRules(SAMPLE_RULE);
+
+    page.selectFacetItem("languages", XOO_LANG)
+      .shouldNotDisplayRules(SAMPLE_RULE_XOO2);
+
+    page.clearAllFilters()
+      .shouldDisplayRules(SAMPLE_RULE, SAMPLE_RULE_XOO2, SAMPLE_RULE2);
+  }
+
+  @Test
+  public void should_load_more_rules() {
+    // TODO
+  }
+
+  @Test
+  public void should_filter_similar_rules() {
+    RulesPage page = tester.openBrowser().openRules();
+    page.shouldDisplayRules(SAMPLE_RULE, SAMPLE_SECURITY_RULE);
+    page.takeRule(SAMPLE_SECURITY_RULE).filterSimilarRules("type");
+    page.shouldNotDisplayRules(SAMPLE_RULE).shouldDisplayRules(SAMPLE_SECURITY_RULE);
+    assertThat(url()).contains("types=VULNERABILITY");
+  }
+
+  @Test
+  public void should_display_rule_details() {
+    RulesPage page = tester.openBrowser().openRules();
+    RuleDetails ruleDetails = page.takeRule(SAMPLE_RULE).open();
+    ruleDetails
+      .shouldHaveType("Code Smell")
+      .shouldHaveSeverity("Major")
+      .shouldHaveDescription("Search for a given tag in Xoo files")
+      .tags().shouldHaveNoTags();
+  }
+
   @Test
-  public void should_display_rule_profiles() {
+  public void should_display_rule_issues() {
+    analyzeProjectWithIssues();
+
     RulesPage page = tester.openBrowser().openRules();
-    page.openFacet("qprofile").selectFacetItemByText("qprofile", "Basic").shouldHaveTotalRules(1);
-    page.openFirstRule().shouldBeActivatedOn("Basic");
+    page.selectFacetItem("languages", XOO_LANG);
+    page.takeRule("xoo:OneIssuePerLine").open()
+      .shouldHaveTotalIssues(17).shouldHaveIssuesOnProject("Sample", 17);
+  }
+
+  @Test
+  public void should_extend_rule_description() {
+    RuleDetails ruleDetails = openRulesAsAdmin().openFirstRule();
+    ruleDetails.extendDescription().cancel();
+    ruleDetails.extendDescription().type("my extended description").submit();
+    ruleDetails.extendDescription().type("another description").submit();
+    ruleDetails.extendDescription().remove();
+  }
+
+  @Test
+  public void should_change_rule_tags() {
+    RuleDetails ruleDetails = openRulesAsAdmin().takeRule(SAMPLE_RULE2).open();
+    ruleDetails.tags()
+      .shouldHaveTags("bad-practice")
+      .edit().search("foo").select("+ foo").done()
+      .edit().select("convention").done()
+      .shouldHaveTags("bad-practice", "convention", "foo");
+  }
+
+  @Test
+  public void should_create_edit_delete_reactivate_custom_rule() {
+    String customRuleName = "custom_rule_name";
+    String customRuleKey = "xoo:" + customRuleName;
+    RuleDetails ruleDetails = openRulesAsAdmin().takeRule(SAMPLE_TEMPLATE_RULE).open();
+
+    ruleDetails.createCustomRule(customRuleName).shouldHaveCustomRule(customRuleKey);
+    ruleDetails.deleteCustomRule(customRuleKey).shouldNotHaveCustomRule(customRuleKey);
+    ruleDetails.reactivateCustomRule(customRuleName).shouldHaveCustomRule(customRuleKey);
+  }
+
+  @Test
+  public void should_activate_deactivate_rule_from_list() {
+    QualityProfile profile = createQualityProfile();
+
+    RulesPage page = openRulesAsAdmin();
+    page.openFacet("profile").selectFacetItem("profile", profile.getKey()).selectInactive();
+    page.shouldDisplayRules(SAMPLE_SECURITY_RULE);
+
+    page.activateRule(SAMPLE_SECURITY_RULE).deactivateRule(SAMPLE_SECURITY_RULE);
+  }
+
+  @Test
+  public void should_activate_rule_from_details() {
+    // make sure we have at least two quality profiles, so we can choose one of them in the select
+    createQualityProfile();
+    QualityProfile profile = createQualityProfile();
+
+    RulesPage page = openRulesAsAdmin();
+    RuleDetails ruleDetails = page.takeRule(SAMPLE_SECURITY_RULE).open();
+    ruleDetails.shouldNotBeActivatedOn(profile.getName()).activate().select(profile.getName()).save();
+    ruleDetails.shouldBeActivatedOn(profile.getKey());
+  }
+
+  @Test
+  public void should_synchronize_activation_between_list_and_details() {
+    // make sure we have at least two quality profiles, so we can choose one of them in the select
+    createQualityProfile();
+    QualityProfile profile = createQualityProfile();
+    RulesPage page = openRulesAsAdmin();
+
+    page.openFacet("profile").selectFacetItem("profile", profile.getKey()).selectInactive();
+    page.shouldDisplayRules(SAMPLE_SECURITY_RULE);
+
+    RuleDetails ruleDetails = page.takeRule(SAMPLE_SECURITY_RULE).open();
+
+    ruleDetails.shouldNotBeActivatedOn(profile.getKey())
+      .activate().select(profile.getKey()).save();
+
+    ruleDetails.shouldBeActivatedOn(profile.getKey());
+
+    page.closeDetails();
+    page.takeRule(SAMPLE_SECURITY_RULE).shouldDisplayDeactivate();
+  }
+
+  @Test
+  public void should_change_rule_activation() {
+    String ruleWithParameters = "xoo:RuleWithParameters";
+    // make sure we have at least two quality profiles, so we can choose one of them in the select
+    createQualityProfile();
+    QualityProfile profile = createQualityProfile();
+    RuleDetails ruleDetails = openRulesAsAdmin().takeRule(ruleWithParameters).open();
+
+    ruleDetails.activate().select(profile.getKey()).save();
+    ruleDetails.shouldBeActivatedOn(profile.getKey());
+
+    ruleDetails.changeActivationOn(profile.getKey())
+      .fill("string", "foo").fill("integer", "123").save();
+
+    ruleDetails
+      .activationShouldHaveParameter(profile.getKey(),"string", "foo")
+      .activationShouldHaveParameter(profile.getKey(),"integer", "123");
+  }
+
+  @Test
+  public void should_revert_rule_activation_to_parent_definition() {
+    QualityProfile profile = createInheritedQualityProfile();
+
+    RuleDetails ruleDetails = openRulesAsAdmin().takeRule(SAMPLE_RULE).open();
+    ruleDetails.activationShouldHaveSeverity(profile.getKey(), "BLOCKER");
+    ruleDetails.revertActivationToParentDefinition(profile.getKey());
+    ruleDetails.activationShouldHaveSeverity(profile.getKey(), "MAJOR");
   }
+
+  private RulesPage openRulesAsAdmin() {
+    String admin = tester.users().generateAdministrator().getLogin();
+    return tester.openBrowser().logIn().submitCredentials(admin).openRules();
+  }
+
+  private String randomQualityProfileName() {
+    return "random_profile_" + String.valueOf(profileIndex++);
+  }
+
+  private QualityProfile createQualityProfile() {
+    return tester.qProfiles().service().create(new CreateRequest().setName(randomQualityProfileName()).setLanguage("xoo")).getProfile();
+  }
+
+  private QualityProfile createInheritedQualityProfile() {
+    QualityProfile profile = createQualityProfile();
+    tester.qProfiles().service().changeParent(new ChangeParentRequest().setQualityProfile(profile.getName()).setLanguage("xoo").setParentQualityProfile("Sonar way"));
+    // activate a rule
+    tester.qProfiles().activateRule(profile, SAMPLE_RULE2);
+    // override a rule
+    tester.qProfiles().activateRule(profile, SAMPLE_RULE, "BLOCKER");
+    return profile;
+  }
+
+  private void analyzeProjectWithIssues() {
+    String qualityProfileKey = projectAnalysisRule.registerProfile("/issue/IssueActionTest/xoo-one-issue-per-line-profile.xml");
+    String projectKey = projectAnalysisRule.registerProject("shared/xoo-sample");
+    projectAnalysisRule.newProjectAnalysis(projectKey).withQualityProfile(qualityProfileKey).run();
+  }
+
+  private void createBlockerCustomRule(String ruleKey) {
+    tester.wsClient().rules().create(new org.sonarqube.ws.client.rules.CreateRequest()
+      .setTemplateKey(SAMPLE_TEMPLATE_RULE)
+      .setSeverity("BLOCKER")
+      .setMarkdownDescription(ruleKey).setName(ruleKey).setCustomKey(ruleKey));
+  }
+
+  private void deleteCustomRule(String ruleKey) {
+    tester.wsClient().rules().delete(new org.sonarqube.ws.client.rules.DeleteRequest().setKey(ruleKey));
+  }
+
 }
index 4d9242c77de1803ddb85d886c7f5ae258759f960..f70da63a7c92871815cf3ba95d49ab921c1820bf 100644 (file)
@@ -55,7 +55,7 @@
 </tr>
 <tr>
        <td>assertElementPresent</td>
-       <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules a[href^=&quot;/organizations/test-org/rules#qprofile&quot;]</td>
+       <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules a[href^=&quot;/organizations/test-org/rules?activation=true&amp;qprofile&quot;]</td>
        <td></td>
 </tr>
 <tr>
index 66f83c9af47b6aa52f72f56dbf807dd88a57b023..95338d9ff2aeb9501bd678e2512b2e497f1830ef 100644 (file)
 </tr>
 <tr>
        <td>assertElementPresent</td>
-       <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules a[href^=&quot;/coding_rules#qprofile&quot;]</td>
+       <td>css=table[data-language=&quot;xoo&quot;] tr[data-name=&quot;Basic&quot;] .quality-profiles-table-rules a[href^=&quot;/coding_rules?&quot;]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertElementPresent</td>
+       <td>css=table[data-language="xoo"] tr[data-name="Basic"] .quality-profiles-table-rules a[href*="qprofile="]</td>
        <td></td>
 </tr>
 <tr>