import { getPendingPlugins } from '../../api/plugins';
import { getSystemStatus, waitSystemUPStatus } from '../../api/system';
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
-import { translate } from '../../helpers/l10n';
+import { translate, translateWithParameters } from '../../helpers/l10n';
import { AdminPagesContext } from '../../types/admin';
import { AppState } from '../../types/appstate';
import { PendingPluginResult } from '../../types/plugins';
}
const { pendingPlugins, systemStatus } = this.state;
- const defaultTitle = translate('layout.settings');
-
const adminPagesContext: AdminPagesContext = { adminPages };
return (
<div>
- <Helmet defaultTitle={defaultTitle} defer={false} titleTemplate={`%s - ${defaultTitle}`} />
+ <Helmet
+ defer={false}
+ titleTemplate={translateWithParameters(
+ 'page_title.template.with_category',
+ translate('layout.settings')
+ )}
+ />
<SettingsNav
extensions={adminPages}
fetchPendingPlugins={this.fetchPendingPlugins}
exports[`should render correctly 1`] = `
<div>
<Helmet
- defaultTitle="layout.settings"
defer={false}
encodeSpecialCharacters={true}
prioritizeSeoTags={false}
- titleTemplate="%s - layout.settings"
+ titleTemplate="page_title.template.with_category.layout.settings"
/>
<WithLocation
extensions={[]}
*/
import * as React from 'react';
import { render } from 'react-dom';
-import { HelmetProvider } from 'react-helmet-async';
+import { Helmet, HelmetProvider } from 'react-helmet-async';
import { IntlProvider } from 'react-intl';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import accountRoutes from '../../apps/account/routes';
import usersRoutes from '../../apps/users/routes';
import webAPIRoutes from '../../apps/web-api/routes';
import webhooksRoutes from '../../apps/webhooks/routes';
+import { translate } from '../../helpers/l10n';
import { getBaseUrl } from '../../helpers/system';
import { AppState } from '../../types/appstate';
import { Feature } from '../../types/features';
<IntlProvider defaultLocale={lang} locale={lang}>
<GlobalMessagesContainer />
<BrowserRouter basename={getBaseUrl()}>
+ <Helmet titleTemplate={translate('page_title.template.default')} />
<Routes>
{renderRedirects()}
expect(
await screen.findByRole('heading', { level: 1, name: 'Awsome java rule' })
).toBeInTheDocument();
- expect(document.title).toEqual('coding_rule.page.Java.Awsome java rule');
+ expect(document.title).toEqual('page_title.template.with_category.coding_rules.page');
expect(screen.getByText('Why')).toBeInTheDocument();
expect(screen.getByText('Because')).toBeInTheDocument();
});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { keyBy } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import { Profile, searchQualityProfiles } from '../../../api/quality-profiles';
-import { getRulesApp, searchRules } from '../../../api/rules';
-import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
-import FiltersHeader from '../../../components/common/FiltersHeader';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import ListFooter from '../../../components/controls/ListFooter';
-import SearchBox from '../../../components/controls/SearchBox';
-import Suggestions from '../../../components/embed-docs-modal/Suggestions';
-import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import BackIcon from '../../../components/icons/BackIcon';
-import '../../../components/search-navigator.css';
-import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
-import { KeyboardKeys } from '../../../helpers/keycodes';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import {
- addSideBarClass,
- addWhitePageClass,
- removeSideBarClass,
- removeWhitePageClass,
-} from '../../../helpers/pages';
-import { SecurityStandard } from '../../../types/security';
-import { Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types';
-import { CurrentUser, isLoggedIn } from '../../../types/users';
-import {
- shouldOpenSonarSourceSecurityFacet,
- shouldOpenStandardsChildFacet,
- shouldOpenStandardsFacet,
- STANDARDS,
-} from '../../issues/utils';
-import {
- Activation,
- Actives,
- areQueriesEqual,
- FacetKey,
- Facets,
- getAppFacet,
- getOpen,
- getSelected,
- getServerFacet,
- hasRuleKey,
- OpenFacets,
- parseQuery,
- Query,
- serializeQuery,
- shouldRequestFacet,
-} from '../query';
-import '../styles.css';
-import BulkChange from './BulkChange';
-import FacetsList from './FacetsList';
-import PageActions from './PageActions';
-import RuleDetails from './RuleDetails';
-import RuleListItem from './RuleListItem';
-
-const PAGE_SIZE = 100;
-const MAX_SEARCH_LENGTH = 200;
-const LIMIT_BEFORE_LOAD_MORE = 5;
-
-interface Props {
- currentUser: CurrentUser;
- location: Location;
- router: Router;
-}
-
-interface State {
- actives?: Actives;
- canWrite?: boolean;
- facets?: Facets;
- loading: boolean;
- openFacets: OpenFacets;
- paging?: Paging;
- referencedProfiles: Dict<Profile>;
- referencedRepositories: Dict<{ key: string; language: string; name: string }>;
- rules: Rule[];
-}
-
-export class App extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- const query = parseQuery(props.location.query);
- this.state = {
- loading: true,
- openFacets: {
- languages: true,
- owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
- 'owaspTop10-2021': shouldOpenStandardsChildFacet(
- {},
- query,
- SecurityStandard.OWASP_TOP10_2021
- ),
- sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25),
- sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
- standards: shouldOpenStandardsFacet({}, query),
- types: true,
- },
- referencedProfiles: {},
- referencedRepositories: {},
- rules: [],
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- addWhitePageClass();
- addSideBarClass();
- this.attachShortcuts();
- this.fetchInitialData();
- }
-
- componentDidUpdate(prevProps: Props) {
- if (!areQueriesEqual(prevProps.location.query, this.props.location.query)) {
- this.fetchFirstRules();
- }
- if (this.getSelectedRuleKey(prevProps) !== this.getSelectedRuleKey(this.props)) {
- // if user simply selected another issue
- // or if user went from the source code back to the list of issues
- this.scrollToSelectedRule();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- removeWhitePageClass();
- removeSideBarClass();
- this.detachShortcuts();
- }
-
- attachShortcuts = () => {
- document.addEventListener('keydown', this.handleKeyPress);
- };
-
- handleKeyPress = (event: KeyboardEvent) => {
- if (isInput(event) || isShortcut(event)) {
- return true;
- }
- switch (event.key) {
- case KeyboardKeys.LeftArrow:
- event.preventDefault();
- this.handleBack();
- break;
- case KeyboardKeys.RightArrow:
- event.preventDefault();
- this.openSelectedRule();
- break;
- case KeyboardKeys.DownArrow:
- event.preventDefault();
- this.selectNextRule();
- break;
- case KeyboardKeys.UpArrow:
- event.preventDefault();
- this.selectPreviousRule();
- break;
- }
- };
-
- detachShortcuts = () => {
- document.removeEventListener('keydown', this.handleKeyPress);
- };
-
- getOpenRule = (rules: Rule[]) => {
- const open = getOpen(this.props.location.query);
- return open && rules.find((rule) => rule.key === open);
- };
-
- getSelectedRuleKey = (props: Props) => {
- return getSelected(props.location.query);
- };
-
- getFacetsToFetch = () => {
- const { openFacets } = this.state;
- return Object.keys(openFacets)
- .filter((facet: FacetKey) => 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 (parseQuery(this.props.location.query).profile) {
- fields.push('actives', 'params');
- }
- return fields;
- };
-
- getSearchParameters = () => ({
- f: this.getFieldsToFetch().join(),
- facets: this.getFacetsToFetch().join(),
- ps: PAGE_SIZE,
- s: 'name',
- ...this.props.location.query,
- });
-
- stopLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- };
-
- fetchInitialData = () => {
- this.setState({ loading: true });
- Promise.all([getRulesApp(), searchQualityProfiles()]).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(rules);
- const selected = rules.length > 0 && !openRule ? rules[0].key : undefined;
- this.routeSelectedRulePath(selected);
- this.setState({
- actives,
- facets,
- loading: false,
- paging,
- rules,
- });
- }
- }, 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: State) => ({
- actives: { ...state.actives, ...actives },
- loading: false,
- paging,
- rules: [...state.rules, ...rules],
- }));
- }
- },
- this.stopLoading
- );
- }
- };
-
- fetchFacet = (facet: FacetKey) => {
- this.makeFetchRequest({ ps: 1, facets: getServerFacet(facet) }).then(({ facets }) => {
- if (this.mounted) {
- this.setState((state) => ({ facets: { ...state.facets, ...facets }, loading: false }));
- }
- }, this.stopLoading);
- };
-
- getSelectedIndex = ({ rules } = this.state) => {
- const selected = this.getSelectedRuleKey(this.props) || getOpen(this.props.location.query);
- const index = rules.findIndex((rule) => rule.key === selected);
- return index !== -1 ? index : undefined;
- };
-
- selectNextRule = () => {
- const { rules, loading, paging } = this.state;
- const selectedIndex = this.getSelectedIndex();
- if (selectedIndex !== undefined) {
- if (
- selectedIndex > rules.length - LIMIT_BEFORE_LOAD_MORE &&
- !loading &&
- paging &&
- rules.length < paging.total
- ) {
- this.fetchMoreRules();
- }
- if (rules && selectedIndex < rules.length - 1) {
- if (this.getOpenRule(this.state.rules)) {
- this.openRule(rules[selectedIndex + 1].key);
- } else {
- this.routeSelectedRulePath(rules[selectedIndex + 1].key);
- }
- }
- }
- };
-
- selectPreviousRule = () => {
- const { rules } = this.state;
- const selectedIndex = this.getSelectedIndex();
- if (rules && selectedIndex !== undefined && selectedIndex > 0) {
- if (this.getOpenRule(this.state.rules)) {
- this.openRule(rules[selectedIndex - 1].key);
- } else {
- this.routeSelectedRulePath(rules[selectedIndex - 1].key);
- }
- }
- };
-
- getRulePath = (rule: string) => ({
- pathname: this.props.location.pathname,
- query: {
- ...serializeQuery(parseQuery(this.props.location.query)),
- open: rule,
- },
- });
-
- routeSelectedRulePath = (rule?: string) => {
- if (rule) {
- this.props.router.replace({
- pathname: this.props.location.pathname,
- query: { ...serializeQuery(parseQuery(this.props.location.query)), selected: rule },
- });
- }
- };
-
- openRule = (rule: string) => {
- const path = this.getRulePath(rule);
- if (this.getOpenRule(this.state.rules)) {
- this.props.router.replace(path);
- } else {
- this.props.router.push(path);
- }
- };
-
- openSelectedRule = () => {
- const selected = this.getSelectedRuleKey(this.props);
- if (selected) {
- this.openRule(selected);
- }
- };
-
- closeRule = () => {
- this.props.router.push({
- pathname: this.props.location.pathname,
- query: {
- ...serializeQuery(parseQuery(this.props.location.query)),
- selected: this.getOpenRule(this.state.rules)?.key || this.getSelectedRuleKey(this.props),
- open: undefined,
- },
- });
- this.scrollToSelectedRule();
- };
-
- scrollToSelectedRule = () => {
- const selected = this.getSelectedRuleKey(this.props);
- if (selected) {
- const element = document.querySelector(`[data-rule="${selected}"]`);
- if (element) {
- element.scrollIntoView({ behavior: 'auto', block: 'center' });
- }
- }
- };
-
- getRuleActivation = (rule: string) => {
- const { actives } = this.state;
- const query = parseQuery(this.props.location.query);
- if (actives && actives[rule] && query.profile) {
- return actives[rule][query.profile];
- }
- };
-
- getSelectedProfile = () => {
- const { referencedProfiles } = this.state;
- const query = parseQuery(this.props.location.query);
- if (query.profile) {
- return referencedProfiles[query.profile];
- }
- };
-
- closeFacet = (facet: string) =>
- this.setState((state) => ({
- openFacets: { ...state.openFacets, [facet]: false },
- }));
-
- handleRuleOpen = (ruleKey: string) => {
- this.props.router.push(this.getRulePath(ruleKey));
- };
-
- handleBack = (event?: React.SyntheticEvent<HTMLAnchorElement>) => {
- const usingPermalink = hasRuleKey(this.props.location.query);
-
- if (event) {
- event.preventDefault();
- event.currentTarget.blur();
- }
-
- if (usingPermalink) {
- this.handleReset();
- } else {
- this.closeRule();
- }
- };
-
- handleFilterChange = (changes: Partial<Query>) => {
- this.props.router.push({
- pathname: this.props.location.pathname,
- query: serializeQuery({ ...parseQuery(this.props.location.query), ...changes }),
- });
-
- this.setState(({ openFacets }) => ({
- openFacets: {
- ...openFacets,
- sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet(openFacets, changes),
- standards: shouldOpenStandardsFacet(openFacets, changes),
- },
- }));
- };
-
- handleFacetToggle = (property: string) => {
- this.setState((state) => {
- const willOpenProperty = !state.openFacets[property];
- const newState = {
- loading: state.loading,
- openFacets: { ...state.openFacets, [property]: willOpenProperty },
- };
-
- // Try to open sonarsource security "subfacet" by default if the standard facet is open
- if (willOpenProperty && property === STANDARDS) {
- newState.openFacets.sonarsourceSecurity = shouldOpenSonarSourceSecurityFacet(
- newState.openFacets,
- parseQuery(this.props.location.query)
- );
- // Force loading of sonarsource security facet data
- property = newState.openFacets.sonarsourceSecurity ? 'sonarsourceSecurity' : property;
- }
-
- if (shouldRequestFacet(property) && (!state.facets || !state.facets[property])) {
- newState.loading = true;
- this.fetchFacet(property);
- }
-
- return newState;
- });
- };
-
- handleReload = () => this.fetchFirstRules();
-
- handleReset = () => this.props.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 (parseQuery(this.props.location.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);
- this.routeSelectedRulePath(selected);
- return { rules };
- });
- 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) => {
- const { actives } = state;
- if (actives && actives[rule]) {
- const newRule = { ...actives[rule] };
- delete newRule[profile];
- return { actives: { ...actives, [rule]: newRule } };
- }
- return null;
- });
-
- handleSearch = (searchQuery: string) => this.handleFilterChange({ searchQuery });
-
- isFiltered = () => Object.keys(serializeQuery(parseQuery(this.props.location.query))).length > 0;
-
- renderBulkButton = () => {
- const { currentUser } = this.props;
- const { canWrite, paging, referencedProfiles } = this.state;
- const query = parseQuery(this.props.location.query);
- const canUpdate = canWrite || Object.values(referencedProfiles).some((p) => p.actions?.edit);
-
- if (!isLoggedIn(currentUser) || !canUpdate) {
- return <div />;
- }
-
- return (
- paging && (
- <BulkChange query={query} referencedProfiles={referencedProfiles} total={paging.total} />
- )
- );
- };
-
- render() {
- const { paging, rules } = this.state;
- const selectedIndex = this.getSelectedIndex();
- const query = parseQuery(this.props.location.query);
- const openRule = this.getOpenRule(this.state.rules);
- const usingPermalink = hasRuleKey(this.props.location.query);
- const selected = this.getSelectedRuleKey(this.props);
-
- return (
- <>
- <Suggestions suggestions="coding_rules" />
- <Helmet
- defer={false}
- title={
- openRule
- ? translateWithParameters('coding_rule.page', openRule.langName, openRule.name)
- : translate('coding_rules.page')
- }
- >
- <meta content="noindex" name="robots" />
- </Helmet>
- <div className="layout-page" id="coding-rules-page">
- <ScreenPositionHelper className="layout-page-side-outer">
- {({ top }) => (
- <section
- aria-label={translate('filters')}
- className="layout-page-side"
- style={{ top }}
- >
- <div className="layout-page-side-inner">
- <div className="layout-page-filters">
- <A11ySkipTarget
- anchor="rules_filters"
- label={translate('coding_rules.skip_to_filters')}
- weight={10}
- />
- <FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
- <SearchBox
- className="spacer-bottom"
- id="coding-rules-search"
- maxLength={MAX_SEARCH_LENGTH}
- minLength={2}
- onChange={this.handleSearch}
- placeholder={translate('search.search_for_rules')}
- value={query.searchQuery || ''}
- />
- <FacetsList
- facets={this.state.facets}
- onFacetToggle={this.handleFacetToggle}
- onFilterChange={this.handleFilterChange}
- openFacets={this.state.openFacets}
- query={query}
- referencedProfiles={this.state.referencedProfiles}
- referencedRepositories={this.state.referencedRepositories}
- selectedProfile={this.getSelectedProfile()}
- />
- </div>
- </div>
- </section>
- )}
- </ScreenPositionHelper>
-
- <main 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">
- <A11ySkipTarget anchor="rules_main" />
- <div className="display-flex-space-between">
- {openRule ? (
- <a
- className="js-back display-inline-flex-center link-no-underline"
- href="#"
- onClick={this.handleBack}
- >
- <BackIcon className="spacer-right" />
- {usingPermalink
- ? translate('coding_rules.see_all')
- : translate('coding_rules.return_to_list')}
- </a>
- ) : (
- this.renderBulkButton()
- )}
- {!usingPermalink && (
- <PageActions paging={paging} selectedIndex={selectedIndex} />
- )}
- </div>
- </div>
- </div>
- </div>
-
- <div className="layout-page-main-inner">
- {openRule ? (
- <RuleDetails
- allowCustomRules={true}
- canWrite={this.state.canWrite}
- onActivate={this.handleRuleActivate}
- onDeactivate={this.handleRuleDeactivate}
- onDelete={this.handleRuleDelete}
- onFilterChange={this.handleFilterChange}
- referencedProfiles={this.state.referencedProfiles}
- referencedRepositories={this.state.referencedRepositories}
- ruleKey={openRule.key}
- selectedProfile={this.getSelectedProfile()}
- />
- ) : (
- <>
- <h2 className="a11y-hidden">{translate('list_of_rules')}</h2>
- <ul>
- {rules.map((rule) => (
- <RuleListItem
- activation={this.getRuleActivation(rule.key)}
- isLoggedIn={isLoggedIn(this.props.currentUser)}
- key={rule.key}
- onActivate={this.handleRuleActivate}
- onDeactivate={this.handleRuleDeactivate}
- onFilterChange={this.handleFilterChange}
- onOpen={this.handleRuleOpen}
- rule={rule}
- selected={rule.key === selected}
- selectedProfile={this.getSelectedProfile()}
- />
- ))}
- </ul>
- {paging !== undefined && (
- <ListFooter
- count={rules.length}
- loadMore={this.fetchMoreRules}
- ready={!this.state.loading}
- total={paging.total}
- />
- )}
- </>
- )}
- </div>
- </main>
- </div>
- </>
- );
- }
-}
-
-function parseActives(rawActives: Dict<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: Dict<number> = {};
- for (const rawValue of rawFacet.values) {
- values[rawValue.val] = rawValue.count;
- }
- facets[getAppFacet(rawFacet.property)] = values;
- }
- return facets;
-}
-
-export default withRouter(withCurrentUserContext(App));
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { keyBy } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import { Profile, searchQualityProfiles } from '../../../api/quality-profiles';
+import { getRulesApp, searchRules } from '../../../api/rules';
+import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
+import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import FiltersHeader from '../../../components/common/FiltersHeader';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import ListFooter from '../../../components/controls/ListFooter';
+import SearchBox from '../../../components/controls/SearchBox';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import BackIcon from '../../../components/icons/BackIcon';
+import '../../../components/search-navigator.css';
+import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
+import { KeyboardKeys } from '../../../helpers/keycodes';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import {
+ addSideBarClass,
+ addWhitePageClass,
+ removeSideBarClass,
+ removeWhitePageClass,
+} from '../../../helpers/pages';
+import { SecurityStandard } from '../../../types/security';
+import { Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types';
+import { CurrentUser, isLoggedIn } from '../../../types/users';
+import {
+ shouldOpenSonarSourceSecurityFacet,
+ shouldOpenStandardsChildFacet,
+ shouldOpenStandardsFacet,
+ STANDARDS,
+} from '../../issues/utils';
+import {
+ Activation,
+ Actives,
+ areQueriesEqual,
+ FacetKey,
+ Facets,
+ getAppFacet,
+ getOpen,
+ getSelected,
+ getServerFacet,
+ hasRuleKey,
+ OpenFacets,
+ parseQuery,
+ Query,
+ serializeQuery,
+ shouldRequestFacet,
+} from '../query';
+import '../styles.css';
+import BulkChange from './BulkChange';
+import FacetsList from './FacetsList';
+import PageActions from './PageActions';
+import RuleDetails from './RuleDetails';
+import RuleListItem from './RuleListItem';
+
+const PAGE_SIZE = 100;
+const MAX_SEARCH_LENGTH = 200;
+const LIMIT_BEFORE_LOAD_MORE = 5;
+
+interface Props {
+ currentUser: CurrentUser;
+ location: Location;
+ router: Router;
+}
+
+interface State {
+ actives?: Actives;
+ canWrite?: boolean;
+ facets?: Facets;
+ loading: boolean;
+ openFacets: OpenFacets;
+ paging?: Paging;
+ referencedProfiles: Dict<Profile>;
+ referencedRepositories: Dict<{ key: string; language: string; name: string }>;
+ rules: Rule[];
+}
+
+export class CodingRulesApp extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ const query = parseQuery(props.location.query);
+ this.state = {
+ loading: true,
+ openFacets: {
+ languages: true,
+ owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
+ 'owaspTop10-2021': shouldOpenStandardsChildFacet(
+ {},
+ query,
+ SecurityStandard.OWASP_TOP10_2021
+ ),
+ sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25),
+ sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
+ standards: shouldOpenStandardsFacet({}, query),
+ types: true,
+ },
+ referencedProfiles: {},
+ referencedRepositories: {},
+ rules: [],
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ addWhitePageClass();
+ addSideBarClass();
+ this.attachShortcuts();
+ this.fetchInitialData();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (!areQueriesEqual(prevProps.location.query, this.props.location.query)) {
+ this.fetchFirstRules();
+ }
+ if (this.getSelectedRuleKey(prevProps) !== this.getSelectedRuleKey(this.props)) {
+ // if user simply selected another issue
+ // or if user went from the source code back to the list of issues
+ this.scrollToSelectedRule();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ removeWhitePageClass();
+ removeSideBarClass();
+ this.detachShortcuts();
+ }
+
+ attachShortcuts = () => {
+ document.addEventListener('keydown', this.handleKeyPress);
+ };
+
+ handleKeyPress = (event: KeyboardEvent) => {
+ if (isInput(event) || isShortcut(event)) {
+ return true;
+ }
+ switch (event.key) {
+ case KeyboardKeys.LeftArrow:
+ event.preventDefault();
+ this.handleBack();
+ break;
+ case KeyboardKeys.RightArrow:
+ event.preventDefault();
+ this.openSelectedRule();
+ break;
+ case KeyboardKeys.DownArrow:
+ event.preventDefault();
+ this.selectNextRule();
+ break;
+ case KeyboardKeys.UpArrow:
+ event.preventDefault();
+ this.selectPreviousRule();
+ break;
+ }
+ };
+
+ detachShortcuts = () => {
+ document.removeEventListener('keydown', this.handleKeyPress);
+ };
+
+ getOpenRule = (rules: Rule[]) => {
+ const open = getOpen(this.props.location.query);
+ return open && rules.find((rule) => rule.key === open);
+ };
+
+ getSelectedRuleKey = (props: Props) => {
+ return getSelected(props.location.query);
+ };
+
+ getFacetsToFetch = () => {
+ const { openFacets } = this.state;
+ return Object.keys(openFacets)
+ .filter((facet: FacetKey) => 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 (parseQuery(this.props.location.query).profile) {
+ fields.push('actives', 'params');
+ }
+ return fields;
+ };
+
+ getSearchParameters = () => ({
+ f: this.getFieldsToFetch().join(),
+ facets: this.getFacetsToFetch().join(),
+ ps: PAGE_SIZE,
+ s: 'name',
+ ...this.props.location.query,
+ });
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ fetchInitialData = () => {
+ this.setState({ loading: true });
+ Promise.all([getRulesApp(), searchQualityProfiles()]).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(rules);
+ const selected = rules.length > 0 && !openRule ? rules[0].key : undefined;
+ this.routeSelectedRulePath(selected);
+ this.setState({
+ actives,
+ facets,
+ loading: false,
+ paging,
+ rules,
+ });
+ }
+ }, 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: State) => ({
+ actives: { ...state.actives, ...actives },
+ loading: false,
+ paging,
+ rules: [...state.rules, ...rules],
+ }));
+ }
+ },
+ this.stopLoading
+ );
+ }
+ };
+
+ fetchFacet = (facet: FacetKey) => {
+ this.makeFetchRequest({ ps: 1, facets: getServerFacet(facet) }).then(({ facets }) => {
+ if (this.mounted) {
+ this.setState((state) => ({ facets: { ...state.facets, ...facets }, loading: false }));
+ }
+ }, this.stopLoading);
+ };
+
+ getSelectedIndex = ({ rules } = this.state) => {
+ const selected = this.getSelectedRuleKey(this.props) || getOpen(this.props.location.query);
+ const index = rules.findIndex((rule) => rule.key === selected);
+ return index !== -1 ? index : undefined;
+ };
+
+ selectNextRule = () => {
+ const { rules, loading, paging } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ if (selectedIndex !== undefined) {
+ if (
+ selectedIndex > rules.length - LIMIT_BEFORE_LOAD_MORE &&
+ !loading &&
+ paging &&
+ rules.length < paging.total
+ ) {
+ this.fetchMoreRules();
+ }
+ if (rules && selectedIndex < rules.length - 1) {
+ if (this.getOpenRule(this.state.rules)) {
+ this.openRule(rules[selectedIndex + 1].key);
+ } else {
+ this.routeSelectedRulePath(rules[selectedIndex + 1].key);
+ }
+ }
+ }
+ };
+
+ selectPreviousRule = () => {
+ const { rules } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ if (rules && selectedIndex !== undefined && selectedIndex > 0) {
+ if (this.getOpenRule(this.state.rules)) {
+ this.openRule(rules[selectedIndex - 1].key);
+ } else {
+ this.routeSelectedRulePath(rules[selectedIndex - 1].key);
+ }
+ }
+ };
+
+ getRulePath = (rule: string) => ({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery(parseQuery(this.props.location.query)),
+ open: rule,
+ },
+ });
+
+ routeSelectedRulePath = (rule?: string) => {
+ if (rule) {
+ this.props.router.replace({
+ pathname: this.props.location.pathname,
+ query: { ...serializeQuery(parseQuery(this.props.location.query)), selected: rule },
+ });
+ }
+ };
+
+ openRule = (rule: string) => {
+ const path = this.getRulePath(rule);
+ if (this.getOpenRule(this.state.rules)) {
+ this.props.router.replace(path);
+ } else {
+ this.props.router.push(path);
+ }
+ };
+
+ openSelectedRule = () => {
+ const selected = this.getSelectedRuleKey(this.props);
+ if (selected) {
+ this.openRule(selected);
+ }
+ };
+
+ closeRule = () => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery(parseQuery(this.props.location.query)),
+ selected: this.getOpenRule(this.state.rules)?.key || this.getSelectedRuleKey(this.props),
+ open: undefined,
+ },
+ });
+ this.scrollToSelectedRule();
+ };
+
+ scrollToSelectedRule = () => {
+ const selected = this.getSelectedRuleKey(this.props);
+ if (selected) {
+ const element = document.querySelector(`[data-rule="${selected}"]`);
+ if (element) {
+ element.scrollIntoView({ behavior: 'auto', block: 'center' });
+ }
+ }
+ };
+
+ getRuleActivation = (rule: string) => {
+ const { actives } = this.state;
+ const query = parseQuery(this.props.location.query);
+ if (actives && actives[rule] && query.profile) {
+ return actives[rule][query.profile];
+ }
+ };
+
+ getSelectedProfile = () => {
+ const { referencedProfiles } = this.state;
+ const query = parseQuery(this.props.location.query);
+ if (query.profile) {
+ return referencedProfiles[query.profile];
+ }
+ };
+
+ closeFacet = (facet: string) =>
+ this.setState((state) => ({
+ openFacets: { ...state.openFacets, [facet]: false },
+ }));
+
+ handleRuleOpen = (ruleKey: string) => {
+ this.props.router.push(this.getRulePath(ruleKey));
+ };
+
+ handleBack = (event?: React.SyntheticEvent<HTMLAnchorElement>) => {
+ const usingPermalink = hasRuleKey(this.props.location.query);
+
+ if (event) {
+ event.preventDefault();
+ event.currentTarget.blur();
+ }
+
+ if (usingPermalink) {
+ this.handleReset();
+ } else {
+ this.closeRule();
+ }
+ };
+
+ handleFilterChange = (changes: Partial<Query>) => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: serializeQuery({ ...parseQuery(this.props.location.query), ...changes }),
+ });
+
+ this.setState(({ openFacets }) => ({
+ openFacets: {
+ ...openFacets,
+ sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet(openFacets, changes),
+ standards: shouldOpenStandardsFacet(openFacets, changes),
+ },
+ }));
+ };
+
+ handleFacetToggle = (property: string) => {
+ this.setState((state) => {
+ const willOpenProperty = !state.openFacets[property];
+ const newState = {
+ loading: state.loading,
+ openFacets: { ...state.openFacets, [property]: willOpenProperty },
+ };
+
+ // Try to open sonarsource security "subfacet" by default if the standard facet is open
+ if (willOpenProperty && property === STANDARDS) {
+ newState.openFacets.sonarsourceSecurity = shouldOpenSonarSourceSecurityFacet(
+ newState.openFacets,
+ parseQuery(this.props.location.query)
+ );
+ // Force loading of sonarsource security facet data
+ property = newState.openFacets.sonarsourceSecurity ? 'sonarsourceSecurity' : property;
+ }
+
+ if (shouldRequestFacet(property) && (!state.facets || !state.facets[property])) {
+ newState.loading = true;
+ this.fetchFacet(property);
+ }
+
+ return newState;
+ });
+ };
+
+ handleReload = () => this.fetchFirstRules();
+
+ handleReset = () => this.props.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 (parseQuery(this.props.location.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);
+ this.routeSelectedRulePath(selected);
+ return { rules };
+ });
+ 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) => {
+ const { actives } = state;
+ if (actives && actives[rule]) {
+ const newRule = { ...actives[rule] };
+ delete newRule[profile];
+ return { actives: { ...actives, [rule]: newRule } };
+ }
+ return null;
+ });
+
+ handleSearch = (searchQuery: string) => this.handleFilterChange({ searchQuery });
+
+ isFiltered = () => Object.keys(serializeQuery(parseQuery(this.props.location.query))).length > 0;
+
+ renderBulkButton = () => {
+ const { currentUser } = this.props;
+ const { canWrite, paging, referencedProfiles } = this.state;
+ const query = parseQuery(this.props.location.query);
+ const canUpdate = canWrite || Object.values(referencedProfiles).some((p) => p.actions?.edit);
+
+ if (!isLoggedIn(currentUser) || !canUpdate) {
+ return <div />;
+ }
+
+ return (
+ paging && (
+ <BulkChange query={query} referencedProfiles={referencedProfiles} total={paging.total} />
+ )
+ );
+ };
+
+ render() {
+ const { paging, rules } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ const query = parseQuery(this.props.location.query);
+ const openRule = this.getOpenRule(this.state.rules);
+ const usingPermalink = hasRuleKey(this.props.location.query);
+ const selected = this.getSelectedRuleKey(this.props);
+
+ return (
+ <>
+ <Suggestions suggestions="coding_rules" />
+ {openRule ? (
+ <Helmet
+ defer={false}
+ title={translateWithParameters('coding_rule.page', openRule.langName, openRule.name)}
+ titleTemplate={translateWithParameters(
+ 'page_title.template.with_category',
+ translate('coding_rules.page')
+ )}
+ />
+ ) : (
+ <Helmet defer={false} title={translate('coding_rules.page')}>
+ <meta content="noindex" name="robots" />
+ </Helmet>
+ )}
+ <div className="layout-page" id="coding-rules-page">
+ <ScreenPositionHelper className="layout-page-side-outer">
+ {({ top }) => (
+ <section
+ aria-label={translate('filters')}
+ className="layout-page-side"
+ style={{ top }}
+ >
+ <div className="layout-page-side-inner">
+ <div className="layout-page-filters">
+ <A11ySkipTarget
+ anchor="rules_filters"
+ label={translate('coding_rules.skip_to_filters')}
+ weight={10}
+ />
+ <FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
+ <SearchBox
+ className="spacer-bottom"
+ id="coding-rules-search"
+ maxLength={MAX_SEARCH_LENGTH}
+ minLength={2}
+ onChange={this.handleSearch}
+ placeholder={translate('search.search_for_rules')}
+ value={query.searchQuery || ''}
+ />
+ <FacetsList
+ facets={this.state.facets}
+ onFacetToggle={this.handleFacetToggle}
+ onFilterChange={this.handleFilterChange}
+ openFacets={this.state.openFacets}
+ query={query}
+ referencedProfiles={this.state.referencedProfiles}
+ referencedRepositories={this.state.referencedRepositories}
+ selectedProfile={this.getSelectedProfile()}
+ />
+ </div>
+ </div>
+ </section>
+ )}
+ </ScreenPositionHelper>
+
+ <main 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">
+ <A11ySkipTarget anchor="rules_main" />
+ <div className="display-flex-space-between">
+ {openRule ? (
+ <a
+ className="js-back display-inline-flex-center link-no-underline"
+ href="#"
+ onClick={this.handleBack}
+ >
+ <BackIcon className="spacer-right" />
+ {usingPermalink
+ ? translate('coding_rules.see_all')
+ : translate('coding_rules.return_to_list')}
+ </a>
+ ) : (
+ this.renderBulkButton()
+ )}
+ {!usingPermalink && (
+ <PageActions paging={paging} selectedIndex={selectedIndex} />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div className="layout-page-main-inner">
+ {openRule ? (
+ <RuleDetails
+ allowCustomRules={true}
+ canWrite={this.state.canWrite}
+ onActivate={this.handleRuleActivate}
+ onDeactivate={this.handleRuleDeactivate}
+ onDelete={this.handleRuleDelete}
+ onFilterChange={this.handleFilterChange}
+ referencedProfiles={this.state.referencedProfiles}
+ referencedRepositories={this.state.referencedRepositories}
+ ruleKey={openRule.key}
+ selectedProfile={this.getSelectedProfile()}
+ />
+ ) : (
+ <>
+ <h2 className="a11y-hidden">{translate('list_of_rules')}</h2>
+ <ul>
+ {rules.map((rule) => (
+ <RuleListItem
+ activation={this.getRuleActivation(rule.key)}
+ isLoggedIn={isLoggedIn(this.props.currentUser)}
+ key={rule.key}
+ onActivate={this.handleRuleActivate}
+ onDeactivate={this.handleRuleDeactivate}
+ onFilterChange={this.handleFilterChange}
+ onOpen={this.handleRuleOpen}
+ rule={rule}
+ selected={rule.key === selected}
+ selectedProfile={this.getSelectedProfile()}
+ />
+ ))}
+ </ul>
+ {paging !== undefined && (
+ <ListFooter
+ count={rules.length}
+ loadMore={this.fetchMoreRules}
+ ready={!this.state.loading}
+ total={paging.total}
+ />
+ )}
+ </>
+ )}
+ </div>
+ </main>
+ </div>
+ </>
+ );
+ }
+}
+
+function parseActives(rawActives: Dict<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: Dict<number> = {};
+ for (const rawValue of rawFacet.values) {
+ values[rawValue.val] = rawValue.count;
+ }
+ facets[getAppFacet(rawFacet.property)] = values;
+ }
+ return facets;
+}
+
+export default withRouter(withCurrentUserContext(CodingRulesApp));
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { searchQualityProfiles } from '../../../../api/quality-profiles';
-import { getRulesApp } from '../../../../api/rules';
-import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
-import {
- mockCurrentUser,
- mockLocation,
- mockQualityProfile,
- mockRouter,
-} from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { App } from '../App';
-
-jest.mock('../../../../components/common/ScreenPositionHelper');
-
-jest.mock('../../../../api/rules', () => {
- const { mockRule } = jest.requireActual('../../../../helpers/testMocks');
- return {
- getRulesApp: jest.fn().mockResolvedValue({ canWrite: true, repositories: [] }),
- searchRules: jest.fn().mockResolvedValue({
- actives: [],
- rawActives: [],
- facets: [],
- rawFacets: [],
- p: 0,
- ps: 100,
- rules: [mockRule(), mockRule()],
- total: 0,
- }),
- };
-});
-
-jest.mock('../../../../api/quality-profiles', () => ({
- searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] }),
-}));
-
-jest.mock('../../../../helpers/system', () => ({
- getReactDomContainerSelector: () => '#content',
-}));
-
-it('should render correctly', async () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot('loading');
-
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot('loaded');
- expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot(
- 'loaded (ScreenPositionHelper)'
- );
-});
-
-describe('renderBulkButton', () => {
- it('should be null when the user is not logged in', () => {
- const wrapper = shallowRender({
- currentUser: mockCurrentUser(),
- });
- expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
- });
-
- it('should be null when the user does not have the sufficient permission', () => {
- (getRulesApp as jest.Mock).mockReturnValueOnce({ canWrite: false, repositories: [] });
-
- const wrapper = shallowRender();
- expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
- });
-
- it('should show bulk change button when user has global admin rights on quality profiles', async () => {
- (getRulesApp as jest.Mock).mockReturnValueOnce({ canWrite: true, repositories: [] });
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
- });
-
- it('should show bulk change button when user has edit rights on specific quality profile', async () => {
- (getRulesApp as jest.Mock).mockReturnValueOnce({ canWrite: false, repositories: [] });
- (searchQualityProfiles as jest.Mock).mockReturnValueOnce({
- profiles: [mockQualityProfile({ key: 'foo', actions: { edit: true } }), mockQualityProfile()],
- });
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
- });
-});
-
-function shallowRender(props: Partial<App['props']> = {}) {
- return shallow<App>(
- <App
- currentUser={mockCurrentUser({
- isLoggedIn: true,
- })}
- location={mockLocation()}
- router={mockRouter()}
- {...props}
- />
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { searchQualityProfiles } from '../../../../api/quality-profiles';
+import { getRulesApp } from '../../../../api/rules';
+import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
+import {
+ mockCurrentUser,
+ mockLocation,
+ mockQualityProfile,
+ mockRouter,
+} from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { CodingRulesApp } from '../CodingRulesApp';
+
+jest.mock('../../../../components/common/ScreenPositionHelper');
+
+jest.mock('../../../../api/rules', () => {
+ const { mockRule } = jest.requireActual('../../../../helpers/testMocks');
+ return {
+ getRulesApp: jest.fn().mockResolvedValue({ canWrite: true, repositories: [] }),
+ searchRules: jest.fn().mockResolvedValue({
+ actives: [],
+ rawActives: [],
+ facets: [],
+ rawFacets: [],
+ p: 0,
+ ps: 100,
+ rules: [mockRule(), mockRule()],
+ total: 0,
+ }),
+ };
+});
+
+jest.mock('../../../../api/quality-profiles', () => ({
+ searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] }),
+}));
+
+jest.mock('../../../../helpers/system', () => ({
+ getReactDomContainerSelector: () => '#content',
+}));
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot('loading');
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot('loaded');
+ expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot(
+ 'loaded (ScreenPositionHelper)'
+ );
+});
+
+describe('renderBulkButton', () => {
+ it('should be null when the user is not logged in', () => {
+ const wrapper = shallowRender({
+ currentUser: mockCurrentUser(),
+ });
+ expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
+ });
+
+ it('should be null when the user does not have the sufficient permission', () => {
+ (getRulesApp as jest.Mock).mockReturnValueOnce({ canWrite: false, repositories: [] });
+
+ const wrapper = shallowRender();
+ expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
+ });
+
+ it('should show bulk change button when user has global admin rights on quality profiles', async () => {
+ (getRulesApp as jest.Mock).mockReturnValueOnce({ canWrite: true, repositories: [] });
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
+ });
+
+ it('should show bulk change button when user has edit rights on specific quality profile', async () => {
+ (getRulesApp as jest.Mock).mockReturnValueOnce({ canWrite: false, repositories: [] });
+ (searchQualityProfiles as jest.Mock).mockReturnValueOnce({
+ profiles: [mockQualityProfile({ key: 'foo', actions: { edit: true } }), mockQualityProfile()],
+ });
+
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.instance().renderBulkButton()).toMatchSnapshot();
+ });
+});
+
+function shallowRender(props: Partial<CodingRulesApp['props']> = {}) {
+ return shallow<CodingRulesApp>(
+ <CodingRulesApp
+ currentUser={mockCurrentUser({
+ isLoggedIn: true,
+ })}
+ location={mockLocation()}
+ router={mockRouter()}
+ {...props}
+ />
+ );
+}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renderBulkButton should be null when the user does not have the sufficient permission 1`] = `<div />`;
-
-exports[`renderBulkButton should be null when the user is not logged in 1`] = `<div />`;
-
-exports[`renderBulkButton should show bulk change button when user has edit rights on specific quality profile 1`] = `
-<BulkChange
- query={
- {
- "activation": undefined,
- "activationSeverities": [],
- "availableSince": undefined,
- "compareToProfile": undefined,
- "cwe": [],
- "inheritance": undefined,
- "languages": [],
- "owaspTop10": [],
- "owaspTop10-2021": [],
- "profile": undefined,
- "repositories": [],
- "ruleKey": undefined,
- "sansTop25": [],
- "searchQuery": undefined,
- "severities": [],
- "sonarsourceSecurity": [],
- "statuses": [],
- "tags": [],
- "template": undefined,
- "types": [],
- }
- }
- referencedProfiles={
- {
- "foo": {
- "actions": {
- "edit": true,
- },
- "activeDeprecatedRuleCount": 2,
- "activeRuleCount": 10,
- "childrenCount": 0,
- "depth": 1,
- "isBuiltIn": false,
- "isDefault": false,
- "isInherited": false,
- "key": "foo",
- "language": "js",
- "languageName": "JavaScript",
- "name": "name",
- "projectCount": 3,
- },
- "key": {
- "activeDeprecatedRuleCount": 2,
- "activeRuleCount": 10,
- "childrenCount": 0,
- "depth": 1,
- "isBuiltIn": false,
- "isDefault": false,
- "isInherited": false,
- "key": "key",
- "language": "js",
- "languageName": "JavaScript",
- "name": "name",
- "projectCount": 3,
- },
- }
- }
- total={0}
-/>
-`;
-
-exports[`renderBulkButton should show bulk change button when user has global admin rights on quality profiles 1`] = `
-<BulkChange
- query={
- {
- "activation": undefined,
- "activationSeverities": [],
- "availableSince": undefined,
- "compareToProfile": undefined,
- "cwe": [],
- "inheritance": undefined,
- "languages": [],
- "owaspTop10": [],
- "owaspTop10-2021": [],
- "profile": undefined,
- "repositories": [],
- "ruleKey": undefined,
- "sansTop25": [],
- "searchQuery": undefined,
- "severities": [],
- "sonarsourceSecurity": [],
- "statuses": [],
- "tags": [],
- "template": undefined,
- "types": [],
- }
- }
- referencedProfiles={{}}
- total={0}
-/>
-`;
-
-exports[`should render correctly: loaded (ScreenPositionHelper) 1`] = `
-<section
- aria-label="filters"
- className="layout-page-side"
- style={
- {
- "top": 0,
- }
- }
->
- <div
- className="layout-page-side-inner"
- >
- <div
- className="layout-page-filters"
- >
- <A11ySkipTarget
- anchor="rules_filters"
- label="coding_rules.skip_to_filters"
- weight={10}
- />
- <FiltersHeader
- displayReset={false}
- onReset={[Function]}
- />
- <SearchBox
- className="spacer-bottom"
- id="coding-rules-search"
- maxLength={200}
- minLength={2}
- onChange={[Function]}
- placeholder="search.search_for_rules"
- value=""
- />
- <FacetsList
- facets={{}}
- onFacetToggle={[Function]}
- onFilterChange={[Function]}
- openFacets={
- {
- "languages": true,
- "owaspTop10": false,
- "owaspTop10-2021": false,
- "sansTop25": false,
- "sonarsourceSecurity": false,
- "standards": false,
- "types": true,
- }
- }
- query={
- {
- "activation": undefined,
- "activationSeverities": [],
- "availableSince": undefined,
- "compareToProfile": undefined,
- "cwe": [],
- "inheritance": undefined,
- "languages": [],
- "owaspTop10": [],
- "owaspTop10-2021": [],
- "profile": undefined,
- "repositories": [],
- "ruleKey": undefined,
- "sansTop25": [],
- "searchQuery": undefined,
- "severities": [],
- "sonarsourceSecurity": [],
- "statuses": [],
- "tags": [],
- "template": undefined,
- "types": [],
- }
- }
- referencedProfiles={{}}
- referencedRepositories={{}}
- />
- </div>
- </div>
-</section>
-`;
-
-exports[`should render correctly: loaded 1`] = `
-<Fragment>
- <Suggestions
- suggestions="coding_rules"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="coding_rules.page"
- >
- <meta
- content="noindex"
- name="robots"
- />
- </Helmet>
- <div
- className="layout-page"
- id="coding-rules-page"
- >
- <ScreenPositionHelper
- className="layout-page-side-outer"
- >
- <Component />
- </ScreenPositionHelper>
- <main
- 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"
- >
- <A11ySkipTarget
- anchor="rules_main"
- />
- <div
- className="display-flex-space-between"
- >
- <BulkChange
- query={
- {
- "activation": undefined,
- "activationSeverities": [],
- "availableSince": undefined,
- "compareToProfile": undefined,
- "cwe": [],
- "inheritance": undefined,
- "languages": [],
- "owaspTop10": [],
- "owaspTop10-2021": [],
- "profile": undefined,
- "repositories": [],
- "ruleKey": undefined,
- "sansTop25": [],
- "searchQuery": undefined,
- "severities": [],
- "sonarsourceSecurity": [],
- "statuses": [],
- "tags": [],
- "template": undefined,
- "types": [],
- }
- }
- referencedProfiles={{}}
- total={0}
- />
- <PageActions
- paging={
- {
- "pageIndex": 0,
- "pageSize": 100,
- "total": 0,
- }
- }
- />
- </div>
- </div>
- </div>
- </div>
- <div
- className="layout-page-main-inner"
- >
- <h2
- className="a11y-hidden"
- >
- list_of_rules
- </h2>
- <ul>
- <RuleListItem
- isLoggedIn={true}
- key="javascript:S1067"
- onActivate={[Function]}
- onDeactivate={[Function]}
- onFilterChange={[Function]}
- onOpen={[Function]}
- rule={
- {
- "key": "javascript:S1067",
- "lang": "js",
- "langName": "JavaScript",
- "name": "Use foo",
- "severity": "MAJOR",
- "status": "READY",
- "sysTags": [
- "a",
- "b",
- ],
- "tags": [
- "x",
- ],
- "type": "CODE_SMELL",
- }
- }
- selected={false}
- />
- <RuleListItem
- isLoggedIn={true}
- key="javascript:S1067"
- onActivate={[Function]}
- onDeactivate={[Function]}
- onFilterChange={[Function]}
- onOpen={[Function]}
- rule={
- {
- "key": "javascript:S1067",
- "lang": "js",
- "langName": "JavaScript",
- "name": "Use foo",
- "severity": "MAJOR",
- "status": "READY",
- "sysTags": [
- "a",
- "b",
- ],
- "tags": [
- "x",
- ],
- "type": "CODE_SMELL",
- }
- }
- selected={false}
- />
- </ul>
- <ListFooter
- count={2}
- loadMore={[Function]}
- ready={true}
- total={0}
- />
- </div>
- </main>
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly: loading 1`] = `
-<Fragment>
- <Suggestions
- suggestions="coding_rules"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="coding_rules.page"
- >
- <meta
- content="noindex"
- name="robots"
- />
- </Helmet>
- <div
- className="layout-page"
- id="coding-rules-page"
- >
- <ScreenPositionHelper
- className="layout-page-side-outer"
- >
- <Component />
- </ScreenPositionHelper>
- <main
- 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"
- >
- <A11ySkipTarget
- anchor="rules_main"
- />
- <div
- className="display-flex-space-between"
- >
- <div />
- <PageActions />
- </div>
- </div>
- </div>
- </div>
- <div
- className="layout-page-main-inner"
- >
- <h2
- className="a11y-hidden"
- >
- list_of_rules
- </h2>
- <ul />
- </div>
- </main>
- </div>
-</Fragment>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renderBulkButton should be null when the user does not have the sufficient permission 1`] = `<div />`;
+
+exports[`renderBulkButton should be null when the user is not logged in 1`] = `<div />`;
+
+exports[`renderBulkButton should show bulk change button when user has edit rights on specific quality profile 1`] = `
+<BulkChange
+ query={
+ {
+ "activation": undefined,
+ "activationSeverities": [],
+ "availableSince": undefined,
+ "compareToProfile": undefined,
+ "cwe": [],
+ "inheritance": undefined,
+ "languages": [],
+ "owaspTop10": [],
+ "owaspTop10-2021": [],
+ "profile": undefined,
+ "repositories": [],
+ "ruleKey": undefined,
+ "sansTop25": [],
+ "searchQuery": undefined,
+ "severities": [],
+ "sonarsourceSecurity": [],
+ "statuses": [],
+ "tags": [],
+ "template": undefined,
+ "types": [],
+ }
+ }
+ referencedProfiles={
+ {
+ "foo": {
+ "actions": {
+ "edit": true,
+ },
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "foo",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "projectCount": 3,
+ },
+ "key": {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "key",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "projectCount": 3,
+ },
+ }
+ }
+ total={0}
+/>
+`;
+
+exports[`renderBulkButton should show bulk change button when user has global admin rights on quality profiles 1`] = `
+<BulkChange
+ query={
+ {
+ "activation": undefined,
+ "activationSeverities": [],
+ "availableSince": undefined,
+ "compareToProfile": undefined,
+ "cwe": [],
+ "inheritance": undefined,
+ "languages": [],
+ "owaspTop10": [],
+ "owaspTop10-2021": [],
+ "profile": undefined,
+ "repositories": [],
+ "ruleKey": undefined,
+ "sansTop25": [],
+ "searchQuery": undefined,
+ "severities": [],
+ "sonarsourceSecurity": [],
+ "statuses": [],
+ "tags": [],
+ "template": undefined,
+ "types": [],
+ }
+ }
+ referencedProfiles={{}}
+ total={0}
+/>
+`;
+
+exports[`should render correctly: loaded (ScreenPositionHelper) 1`] = `
+<section
+ aria-label="filters"
+ className="layout-page-side"
+ style={
+ {
+ "top": 0,
+ }
+ }
+>
+ <div
+ className="layout-page-side-inner"
+ >
+ <div
+ className="layout-page-filters"
+ >
+ <A11ySkipTarget
+ anchor="rules_filters"
+ label="coding_rules.skip_to_filters"
+ weight={10}
+ />
+ <FiltersHeader
+ displayReset={false}
+ onReset={[Function]}
+ />
+ <SearchBox
+ className="spacer-bottom"
+ id="coding-rules-search"
+ maxLength={200}
+ minLength={2}
+ onChange={[Function]}
+ placeholder="search.search_for_rules"
+ value=""
+ />
+ <FacetsList
+ facets={{}}
+ onFacetToggle={[Function]}
+ onFilterChange={[Function]}
+ openFacets={
+ {
+ "languages": true,
+ "owaspTop10": false,
+ "owaspTop10-2021": false,
+ "sansTop25": false,
+ "sonarsourceSecurity": false,
+ "standards": false,
+ "types": true,
+ }
+ }
+ query={
+ {
+ "activation": undefined,
+ "activationSeverities": [],
+ "availableSince": undefined,
+ "compareToProfile": undefined,
+ "cwe": [],
+ "inheritance": undefined,
+ "languages": [],
+ "owaspTop10": [],
+ "owaspTop10-2021": [],
+ "profile": undefined,
+ "repositories": [],
+ "ruleKey": undefined,
+ "sansTop25": [],
+ "searchQuery": undefined,
+ "severities": [],
+ "sonarsourceSecurity": [],
+ "statuses": [],
+ "tags": [],
+ "template": undefined,
+ "types": [],
+ }
+ }
+ referencedProfiles={{}}
+ referencedRepositories={{}}
+ />
+ </div>
+ </div>
+</section>
+`;
+
+exports[`should render correctly: loaded 1`] = `
+<Fragment>
+ <Suggestions
+ suggestions="coding_rules"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ prioritizeSeoTags={false}
+ title="coding_rules.page"
+ >
+ <meta
+ content="noindex"
+ name="robots"
+ />
+ </Helmet>
+ <div
+ className="layout-page"
+ id="coding-rules-page"
+ >
+ <ScreenPositionHelper
+ className="layout-page-side-outer"
+ >
+ <Component />
+ </ScreenPositionHelper>
+ <main
+ 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"
+ >
+ <A11ySkipTarget
+ anchor="rules_main"
+ />
+ <div
+ className="display-flex-space-between"
+ >
+ <BulkChange
+ query={
+ {
+ "activation": undefined,
+ "activationSeverities": [],
+ "availableSince": undefined,
+ "compareToProfile": undefined,
+ "cwe": [],
+ "inheritance": undefined,
+ "languages": [],
+ "owaspTop10": [],
+ "owaspTop10-2021": [],
+ "profile": undefined,
+ "repositories": [],
+ "ruleKey": undefined,
+ "sansTop25": [],
+ "searchQuery": undefined,
+ "severities": [],
+ "sonarsourceSecurity": [],
+ "statuses": [],
+ "tags": [],
+ "template": undefined,
+ "types": [],
+ }
+ }
+ referencedProfiles={{}}
+ total={0}
+ />
+ <PageActions
+ paging={
+ {
+ "pageIndex": 0,
+ "pageSize": 100,
+ "total": 0,
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="layout-page-main-inner"
+ >
+ <h2
+ className="a11y-hidden"
+ >
+ list_of_rules
+ </h2>
+ <ul>
+ <RuleListItem
+ isLoggedIn={true}
+ key="javascript:S1067"
+ onActivate={[Function]}
+ onDeactivate={[Function]}
+ onFilterChange={[Function]}
+ onOpen={[Function]}
+ rule={
+ {
+ "key": "javascript:S1067",
+ "lang": "js",
+ "langName": "JavaScript",
+ "name": "Use foo",
+ "severity": "MAJOR",
+ "status": "READY",
+ "sysTags": [
+ "a",
+ "b",
+ ],
+ "tags": [
+ "x",
+ ],
+ "type": "CODE_SMELL",
+ }
+ }
+ selected={false}
+ />
+ <RuleListItem
+ isLoggedIn={true}
+ key="javascript:S1067"
+ onActivate={[Function]}
+ onDeactivate={[Function]}
+ onFilterChange={[Function]}
+ onOpen={[Function]}
+ rule={
+ {
+ "key": "javascript:S1067",
+ "lang": "js",
+ "langName": "JavaScript",
+ "name": "Use foo",
+ "severity": "MAJOR",
+ "status": "READY",
+ "sysTags": [
+ "a",
+ "b",
+ ],
+ "tags": [
+ "x",
+ ],
+ "type": "CODE_SMELL",
+ }
+ }
+ selected={false}
+ />
+ </ul>
+ <ListFooter
+ count={2}
+ loadMore={[Function]}
+ ready={true}
+ total={0}
+ />
+ </div>
+ </main>
+ </div>
+</Fragment>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<Fragment>
+ <Suggestions
+ suggestions="coding_rules"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ prioritizeSeoTags={false}
+ title="coding_rules.page"
+ >
+ <meta
+ content="noindex"
+ name="robots"
+ />
+ </Helmet>
+ <div
+ className="layout-page"
+ id="coding-rules-page"
+ >
+ <ScreenPositionHelper
+ className="layout-page-side-outer"
+ >
+ <Component />
+ </ScreenPositionHelper>
+ <main
+ 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"
+ >
+ <A11ySkipTarget
+ anchor="rules_main"
+ />
+ <div
+ className="display-flex-space-between"
+ >
+ <div />
+ <PageActions />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="layout-page-main-inner"
+ >
+ <h2
+ className="a11y-hidden"
+ >
+ list_of_rules
+ </h2>
+ <ul />
+ </div>
+ </main>
+ </div>
+</Fragment>
+`;
import React, { useEffect } from 'react';
import { Route, useLocation, useNavigate } from 'react-router-dom';
import { RawQuery } from '../../types/types';
-import App from './components/App';
+import CodingRulesApp from './components/CodingRulesApp';
import { parseQuery, serializeQuery } from './query';
const EXPECTED_SPLIT_PARTS = 2;
}
}, [location, navigate]);
- return <App />;
+ return <CodingRulesApp />;
}
const routes = () => <Route path="coding_rules" element={<HashEditWrapper />} />;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import styled from '@emotion/styled';
-import { debounce, keyBy } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import { getMeasuresWithPeriod } from '../../../api/measures';
-import { getAllMetrics } from '../../../api/metrics';
-import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
-import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
-import Suggestions from '../../../components/embed-docs-modal/Suggestions';
-import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import { enhanceMeasure } from '../../../components/measure/utils';
-import '../../../components/search-navigator.css';
-import { Alert } from '../../../components/ui/Alert';
-import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
-import {
- getLocalizedMetricDomain,
- translate,
- translateWithParameters,
-} from '../../../helpers/l10n';
-import {
- addSideBarClass,
- addWhitePageClass,
- removeSideBarClass,
- removeWhitePageClass,
-} from '../../../helpers/pages';
-import { BranchLike } from '../../../types/branch-like';
-import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
-import {
- ComponentMeasure,
- Dict,
- Issue,
- MeasureEnhanced,
- Metric,
- Period,
-} from '../../../types/types';
-import Sidebar from '../sidebar/Sidebar';
-import '../style.css';
-import {
- banQualityGateMeasure,
- getMeasuresPageMetricKeys,
- groupByDomains,
- hasBubbleChart,
- hasFullMeasures,
- hasTree,
- hasTreemap,
- isProjectOverview,
- parseQuery,
- Query,
- serializeQuery,
- sortMeasures,
-} from '../utils';
-import MeasureContent from './MeasureContent';
-import MeasureOverviewContainer from './MeasureOverviewContainer';
-import MeasuresEmpty from './MeasuresEmpty';
-
-interface Props {
- branchLike?: BranchLike;
- component: ComponentMeasure;
- fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
- location: Location;
- router: Router;
-}
-
-interface State {
- leakPeriod?: Period;
- loading: boolean;
- measures: MeasureEnhanced[];
- metrics: Dict<Metric>;
-}
-
-export class App extends React.PureComponent<Props, State> {
- mounted = false;
- state: State;
-
- constructor(props: Props) {
- super(props);
- this.state = {
- loading: true,
- measures: [],
- metrics: {},
- };
- this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
- }
-
- componentDidMount() {
- this.mounted = true;
-
- getAllMetrics().then(
- (metrics) => {
- const byKey = keyBy(metrics, 'key');
- this.setState({ metrics: byKey });
- this.fetchMeasures(byKey);
- },
- () => {}
- );
- }
-
- componentDidUpdate(prevProps: Props, prevState: State) {
- const prevQuery = parseQuery(prevProps.location.query);
- const query = parseQuery(this.props.location.query);
-
- if (
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
- prevProps.component.key !== this.props.component.key ||
- prevQuery.selected !== query.selected
- ) {
- this.fetchMeasures(this.state.metrics);
- }
-
- if (prevState.measures.length === 0 && this.state.measures.length > 0) {
- addWhitePageClass();
- addSideBarClass();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- removeWhitePageClass();
- removeSideBarClass();
- }
-
- fetchMeasures(metrics: State['metrics']) {
- const { branchLike } = this.props;
- const query = parseQuery(this.props.location.query);
- const componentKey = query.selected || this.props.component.key;
-
- const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike);
-
- getMeasuresWithPeriod(componentKey, filteredKeys, getBranchLikeQuery(branchLike)).then(
- ({ component, period }) => {
- if (this.mounted) {
- const measures = banQualityGateMeasure(component).map((measure) =>
- enhanceMeasure(measure, metrics)
- );
-
- const leakPeriod =
- component.qualifier === ComponentQualifier.Project ? period : undefined;
-
- this.setState({
- loading: false,
- leakPeriod,
- measures: measures.filter(
- (measure) => measure.value !== undefined || measure.leak !== undefined
- ),
- });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- );
- }
-
- getHelmetTitle = (query: Query, displayOverview: boolean, metric?: Metric) => {
- if (displayOverview && query.metric) {
- return isProjectOverview(query.metric)
- ? translate('component_measures.overview.project_overview.facet')
- : translateWithParameters(
- 'component_measures.domain_x_overview',
- getLocalizedMetricDomain(query.metric)
- );
- }
- return metric ? metric.name : translate('layout.measures');
- };
-
- getSelectedMetric = (query: Query, displayOverview: boolean) => {
- if (displayOverview) {
- return undefined;
- }
- const metric = this.state.metrics[query.metric];
- if (!metric) {
- const domainMeasures = groupByDomains(this.state.measures);
- const firstMeasure =
- domainMeasures[0] && sortMeasures(domainMeasures[0].name, domainMeasures[0].measures)[0];
- if (firstMeasure && typeof firstMeasure !== 'string') {
- return firstMeasure.metric;
- }
- }
- return metric;
- };
-
- handleIssueChange = (_: Issue) => {
- this.refreshBranchStatus();
- };
-
- updateQuery = (newQuery: Partial<Query>) => {
- const query: Query = { ...parseQuery(this.props.location.query), ...newQuery };
-
- const metric = this.getSelectedMetric(query, false);
- if (metric) {
- if (query.view === 'treemap' && !hasTreemap(metric.key, metric.type)) {
- query.view = 'tree';
- } else if (query.view === 'tree' && !hasTree(metric.key)) {
- query.view = 'list';
- }
- }
-
- this.props.router.push({
- pathname: this.props.location.pathname,
- query: {
- ...serializeQuery(query),
- ...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component.key,
- },
- });
- };
-
- refreshBranchStatus = () => {
- const { branchLike, component } = this.props;
- if (branchLike && component && isPullRequest(branchLike)) {
- this.props.fetchBranchStatus(branchLike, component.key);
- }
- };
-
- renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => {
- const { branchLike, component } = this.props;
- const { leakPeriod } = this.state;
- if (displayOverview) {
- return (
- <MeasureOverviewContainer
- branchLike={branchLike}
- className="layout-page-main"
- domain={query.metric}
- leakPeriod={leakPeriod}
- metrics={this.state.metrics}
- onIssueChange={this.handleIssueChange}
- rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- updateQuery={this.updateQuery}
- />
- );
- }
-
- if (!metric) {
- return <MeasuresEmpty />;
- }
-
- const hideDrilldown =
- isPullRequest(branchLike) &&
- (metric.key === 'coverage' || metric.key === 'duplicated_lines_density');
-
- if (hideDrilldown) {
- return (
- <main className="layout-page-main">
- <div className="layout-page-main-inner">
- <div className="note">{translate('component_measures.details_are_not_available')}</div>
- </div>
- </main>
- );
- }
-
- return (
- <MeasureContent
- branchLike={branchLike}
- leakPeriod={leakPeriod}
- metrics={this.state.metrics}
- onIssueChange={this.handleIssueChange}
- requestedMetric={metric}
- rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- asc={query.asc}
- updateQuery={this.updateQuery}
- view={query.view}
- />
- );
- };
-
- render() {
- if (this.state.loading) {
- return (
- <div className="display-flex-justify-center huge-spacer-top">
- <i className="spinner" />
- </div>
- );
- }
-
- const { branchLike } = this.props;
- const { measures } = this.state;
- const { canBrowseAllChildProjects, qualifier } = this.props.component;
- const query = parseQuery(this.props.location.query);
- const showFullMeasures = hasFullMeasures(branchLike);
- const displayOverview = hasBubbleChart(query.metric);
- const metric = this.getSelectedMetric(query, displayOverview);
-
- return (
- <div id="component-measures">
- <Suggestions suggestions="component_measures" />
- <Helmet defer={false} title={this.getHelmetTitle(query, displayOverview, metric)} />
- {measures.length > 0 ? (
- <div className="layout-page">
- <ScreenPositionHelper className="layout-page-side-outer">
- {({ top }) => (
- <div className="layout-page-side" style={{ top }}>
- <div className="layout-page-side-inner">
- {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
- <Alert
- className="big-spacer-top big-spacer-right big-spacer-left it__portfolio_warning"
- variant="warning"
- >
- <AlertContent>
- {translate('component_measures.not_all_measures_are_shown')}
- <HelpTooltip
- className="spacer-left"
- ariaLabel={translate(
- 'component_measures.not_all_measures_are_shown.help'
- )}
- overlay={translate(
- 'component_measures.not_all_measures_are_shown.help'
- )}
- />
- </AlertContent>
- </Alert>
- )}
- <div className="layout-page-filters">
- <Sidebar
- measures={measures}
- selectedMetric={metric ? metric.key : query.metric}
- showFullMeasures={showFullMeasures}
- updateQuery={this.updateQuery}
- />
- </div>
- </div>
- </div>
- )}
- </ScreenPositionHelper>
- {this.renderContent(displayOverview, query, metric)}
- </div>
- ) : (
- <MeasuresEmpty />
- )}
- </div>
- );
- }
-}
-
-const AlertContent = styled.div`
- display: flex;
- align-items: center;
-`;
-
-/*
- * This needs to be refactored: the issue
- * is that we can't use the usual withComponentContext HOC, because the type
- * of `component` isn't the same. It probably used to work because of the lazy loading
- */
-const WrappedApp = withRouter(withBranchStatusActions(App));
-
-function AppWithComponentContext() {
- const { branchLike, component } = React.useContext(ComponentContext);
-
- return <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />;
-}
-
-export default AppWithComponentContext;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import { debounce, keyBy } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import { getMeasuresWithPeriod } from '../../../api/measures';
+import { getAllMetrics } from '../../../api/metrics';
+import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
+import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import { enhanceMeasure } from '../../../components/measure/utils';
+import '../../../components/search-navigator.css';
+import { Alert } from '../../../components/ui/Alert';
+import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import {
+ addSideBarClass,
+ addWhitePageClass,
+ removeSideBarClass,
+ removeWhitePageClass,
+} from '../../../helpers/pages';
+import { BranchLike } from '../../../types/branch-like';
+import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
+import {
+ ComponentMeasure,
+ Dict,
+ Issue,
+ MeasureEnhanced,
+ Metric,
+ Period,
+} from '../../../types/types';
+import Sidebar from '../sidebar/Sidebar';
+import '../style.css';
+import {
+ banQualityGateMeasure,
+ getMeasuresPageMetricKeys,
+ groupByDomains,
+ hasBubbleChart,
+ hasFullMeasures,
+ hasTree,
+ hasTreemap,
+ parseQuery,
+ Query,
+ serializeQuery,
+ sortMeasures,
+} from '../utils';
+import MeasureContent from './MeasureContent';
+import MeasureOverviewContainer from './MeasureOverviewContainer';
+import MeasuresEmpty from './MeasuresEmpty';
+
+interface Props {
+ branchLike?: BranchLike;
+ component: ComponentMeasure;
+ fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
+ location: Location;
+ router: Router;
+}
+
+interface State {
+ leakPeriod?: Period;
+ loading: boolean;
+ measures: MeasureEnhanced[];
+ metrics: Dict<Metric>;
+}
+
+export class ComponentMeasuresApp extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ loading: true,
+ measures: [],
+ metrics: {},
+ };
+ this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+
+ getAllMetrics().then(
+ (metrics) => {
+ const byKey = keyBy(metrics, 'key');
+ this.setState({ metrics: byKey });
+ this.fetchMeasures(byKey);
+ },
+ () => {}
+ );
+ }
+
+ componentDidUpdate(prevProps: Props, prevState: State) {
+ const prevQuery = parseQuery(prevProps.location.query);
+ const query = parseQuery(this.props.location.query);
+
+ if (
+ !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
+ prevProps.component.key !== this.props.component.key ||
+ prevQuery.selected !== query.selected
+ ) {
+ this.fetchMeasures(this.state.metrics);
+ }
+
+ if (prevState.measures.length === 0 && this.state.measures.length > 0) {
+ addWhitePageClass();
+ addSideBarClass();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ removeWhitePageClass();
+ removeSideBarClass();
+ }
+
+ fetchMeasures(metrics: State['metrics']) {
+ const { branchLike } = this.props;
+ const query = parseQuery(this.props.location.query);
+ const componentKey = query.selected || this.props.component.key;
+
+ const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike);
+
+ getMeasuresWithPeriod(componentKey, filteredKeys, getBranchLikeQuery(branchLike)).then(
+ ({ component, period }) => {
+ if (this.mounted) {
+ const measures = banQualityGateMeasure(component).map((measure) =>
+ enhanceMeasure(measure, metrics)
+ );
+
+ const leakPeriod =
+ component.qualifier === ComponentQualifier.Project ? period : undefined;
+
+ this.setState({
+ loading: false,
+ leakPeriod,
+ measures: measures.filter(
+ (measure) => measure.value !== undefined || measure.leak !== undefined
+ ),
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+
+ getSelectedMetric = (query: Query, displayOverview: boolean) => {
+ if (displayOverview) {
+ return undefined;
+ }
+ const metric = this.state.metrics[query.metric];
+ if (!metric) {
+ const domainMeasures = groupByDomains(this.state.measures);
+ const firstMeasure =
+ domainMeasures[0] && sortMeasures(domainMeasures[0].name, domainMeasures[0].measures)[0];
+ if (firstMeasure && typeof firstMeasure !== 'string') {
+ return firstMeasure.metric;
+ }
+ }
+ return metric;
+ };
+
+ handleIssueChange = (_: Issue) => {
+ this.refreshBranchStatus();
+ };
+
+ updateQuery = (newQuery: Partial<Query>) => {
+ const query: Query = { ...parseQuery(this.props.location.query), ...newQuery };
+
+ const metric = this.getSelectedMetric(query, false);
+ if (metric) {
+ if (query.view === 'treemap' && !hasTreemap(metric.key, metric.type)) {
+ query.view = 'tree';
+ } else if (query.view === 'tree' && !hasTree(metric.key)) {
+ query.view = 'list';
+ }
+ }
+
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery(query),
+ ...getBranchLikeQuery(this.props.branchLike),
+ id: this.props.component.key,
+ },
+ });
+ };
+
+ refreshBranchStatus = () => {
+ const { branchLike, component } = this.props;
+ if (branchLike && component && isPullRequest(branchLike)) {
+ this.props.fetchBranchStatus(branchLike, component.key);
+ }
+ };
+
+ renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => {
+ const { branchLike, component } = this.props;
+ const { leakPeriod } = this.state;
+ if (displayOverview) {
+ return (
+ <MeasureOverviewContainer
+ branchLike={branchLike}
+ className="layout-page-main"
+ domain={query.metric}
+ leakPeriod={leakPeriod}
+ metrics={this.state.metrics}
+ onIssueChange={this.handleIssueChange}
+ rootComponent={component}
+ router={this.props.router}
+ selected={query.selected}
+ updateQuery={this.updateQuery}
+ />
+ );
+ }
+
+ if (!metric) {
+ return <MeasuresEmpty />;
+ }
+
+ const hideDrilldown =
+ isPullRequest(branchLike) &&
+ (metric.key === 'coverage' || metric.key === 'duplicated_lines_density');
+
+ if (hideDrilldown) {
+ return (
+ <main className="layout-page-main">
+ <div className="layout-page-main-inner">
+ <div className="note">{translate('component_measures.details_are_not_available')}</div>
+ </div>
+ </main>
+ );
+ }
+
+ return (
+ <MeasureContent
+ branchLike={branchLike}
+ leakPeriod={leakPeriod}
+ metrics={this.state.metrics}
+ onIssueChange={this.handleIssueChange}
+ requestedMetric={metric}
+ rootComponent={component}
+ router={this.props.router}
+ selected={query.selected}
+ asc={query.asc}
+ updateQuery={this.updateQuery}
+ view={query.view}
+ />
+ );
+ };
+
+ render() {
+ if (this.state.loading) {
+ return (
+ <div className="display-flex-justify-center huge-spacer-top">
+ <i className="spinner" />
+ </div>
+ );
+ }
+
+ const { branchLike } = this.props;
+ const { measures } = this.state;
+ const { canBrowseAllChildProjects, qualifier } = this.props.component;
+ const query = parseQuery(this.props.location.query);
+ const showFullMeasures = hasFullMeasures(branchLike);
+ const displayOverview = hasBubbleChart(query.metric);
+ const metric = this.getSelectedMetric(query, displayOverview);
+
+ return (
+ <div id="component-measures">
+ <Suggestions suggestions="component_measures" />
+ <Helmet defer={false} title={translate('layout.measures')} />
+ {measures.length > 0 ? (
+ <div className="layout-page">
+ <ScreenPositionHelper className="layout-page-side-outer">
+ {({ top }) => (
+ <div className="layout-page-side" style={{ top }}>
+ <div className="layout-page-side-inner">
+ {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
+ <Alert
+ className="big-spacer-top big-spacer-right big-spacer-left it__portfolio_warning"
+ variant="warning"
+ >
+ <AlertContent>
+ {translate('component_measures.not_all_measures_are_shown')}
+ <HelpTooltip
+ className="spacer-left"
+ ariaLabel={translate(
+ 'component_measures.not_all_measures_are_shown.help'
+ )}
+ overlay={translate(
+ 'component_measures.not_all_measures_are_shown.help'
+ )}
+ />
+ </AlertContent>
+ </Alert>
+ )}
+ <div className="layout-page-filters">
+ <Sidebar
+ measures={measures}
+ selectedMetric={metric ? metric.key : query.metric}
+ showFullMeasures={showFullMeasures}
+ updateQuery={this.updateQuery}
+ />
+ </div>
+ </div>
+ </div>
+ )}
+ </ScreenPositionHelper>
+ {this.renderContent(displayOverview, query, metric)}
+ </div>
+ ) : (
+ <MeasuresEmpty />
+ )}
+ </div>
+ );
+ }
+}
+
+const AlertContent = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+/*
+ * This needs to be refactored: the issue
+ * is that we can't use the usual withComponentContext HOC, because the type
+ * of `component` isn't the same. It probably used to work because of the lazy loading
+ */
+const WrappedApp = withRouter(withBranchStatusActions(ComponentMeasuresApp));
+
+function AppWithComponentContext() {
+ const { branchLike, component } = React.useContext(ComponentContext);
+
+ return <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />;
+}
+
+export default AppWithComponentContext;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { getMeasuresWithPeriod } from '../../../../api/measures';
-import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
-import { Alert } from '../../../../components/ui/Alert';
-import { mockMainBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockIssue, mockLocation, mockRouter } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { ComponentQualifier } from '../../../../types/component';
-import { App } from '../App';
-
-jest.mock('../../../../api/metrics', () => ({
- getAllMetrics: jest.fn().mockResolvedValue([
- {
- id: '1',
- key: 'lines_to_cover',
- type: 'INT',
- name: 'Lines to Cover',
- domain: 'Coverage',
- },
- {
- id: '2',
- key: 'coverage',
- type: 'PERCENT',
- name: 'Coverage',
- domain: 'Coverage',
- },
- {
- id: '3',
- key: 'duplicated_lines_density',
- type: 'PERCENT',
- name: 'Duplicated Lines (%)',
- domain: 'Duplications',
- },
- {
- id: '4',
- key: 'new_bugs',
- type: 'INT',
- name: 'New Bugs',
- domain: 'Reliability',
- },
- ]),
-}));
-
-jest.mock('../../../../api/measures', () => ({
- getMeasuresWithPeriod: jest.fn(),
-}));
-
-beforeEach(() => {
- (getMeasuresWithPeriod as jest.Mock).mockResolvedValue({
- component: { measures: [{ metric: 'coverage', value: '80.0' }] },
- period: { mode: 'previous_version' },
- });
-});
-
-it('should render correctly', async () => {
- const wrapper = shallowRender();
- expect(wrapper.find('.spinner')).toHaveLength(1);
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render a measure overview', async () => {
- const wrapper = shallowRender({
- location: mockLocation({ pathname: '/component_measures', query: { metric: 'Reliability' } }),
- });
- expect(wrapper.find('.spinner')).toHaveLength(1);
- await waitAndUpdate(wrapper);
- expect(wrapper.find('MeasureOverviewContainer')).toHaveLength(1);
-});
-
-it('should render a message when there are no measures', async () => {
- (getMeasuresWithPeriod as jest.Mock).mockResolvedValue({
- component: { measures: [] },
- period: { mode: 'previous_version' },
- });
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should not render drilldown for estimated duplications', async () => {
- const wrapper = shallowRender({ branchLike: mockPullRequest({ title: '' }) });
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should refresh branch status if issues are updated', async () => {
- const fetchBranchStatus = jest.fn();
- const branchLike = mockPullRequest();
- const wrapper = shallowRender({ branchLike, fetchBranchStatus });
- const instance = wrapper.instance();
- await waitAndUpdate(wrapper);
-
- instance.handleIssueChange(mockIssue());
- expect(fetchBranchStatus).toHaveBeenCalledWith(branchLike, 'foo');
-});
-
-it('should render a warning message when user does not have access to all projects whithin a Portfolio', async () => {
- const wrapper = shallowRender({
- component: mockComponent({
- qualifier: ComponentQualifier.Portfolio,
- canBrowseAllChildProjects: false,
- }),
- });
- await waitAndUpdate(wrapper);
- expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot(
- 'Measure menu with warning (ScreenPositionHelper)'
- );
-});
-
-it.each([
- [ComponentQualifier.Portfolio, true, false],
- [ComponentQualifier.Project, false, false],
- [ComponentQualifier.Portfolio, false, true],
-])(
- 'should not render a warning message',
- async (
- componentQualifier: ComponentQualifier,
- canBrowseAllChildProjects: boolean,
- alertIsVisible: boolean
- ) => {
- const wrapper = shallowRender({
- component: mockComponent({
- qualifier: componentQualifier,
- canBrowseAllChildProjects,
- }),
- });
- await waitAndUpdate(wrapper);
- expect(wrapper.find(ScreenPositionHelper).dive().find(Alert).exists()).toBe(alertIsVisible);
- }
-);
-
-function shallowRender(props: Partial<App['props']> = {}) {
- return shallow<App>(
- <App
- branchLike={mockMainBranch()}
- component={mockComponent({ key: 'foo', name: 'Foo' })}
- fetchBranchStatus={jest.fn()}
- location={mockLocation({ pathname: '/component_measures', query: { metric: 'coverage' } })}
- router={mockRouter()}
- {...props}
- />
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { getMeasuresWithPeriod } from '../../../../api/measures';
+import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
+import { Alert } from '../../../../components/ui/Alert';
+import { mockMainBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockIssue, mockLocation, mockRouter } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { ComponentMeasuresApp } from '../ComponentMeasuresApp';
+
+jest.mock('../../../../api/metrics', () => ({
+ getAllMetrics: jest.fn().mockResolvedValue([
+ {
+ id: '1',
+ key: 'lines_to_cover',
+ type: 'INT',
+ name: 'Lines to Cover',
+ domain: 'Coverage',
+ },
+ {
+ id: '2',
+ key: 'coverage',
+ type: 'PERCENT',
+ name: 'Coverage',
+ domain: 'Coverage',
+ },
+ {
+ id: '3',
+ key: 'duplicated_lines_density',
+ type: 'PERCENT',
+ name: 'Duplicated Lines (%)',
+ domain: 'Duplications',
+ },
+ {
+ id: '4',
+ key: 'new_bugs',
+ type: 'INT',
+ name: 'New Bugs',
+ domain: 'Reliability',
+ },
+ ]),
+}));
+
+jest.mock('../../../../api/measures', () => ({
+ getMeasuresWithPeriod: jest.fn(),
+}));
+
+beforeEach(() => {
+ (getMeasuresWithPeriod as jest.Mock).mockResolvedValue({
+ component: { measures: [{ metric: 'coverage', value: '80.0' }] },
+ period: { mode: 'previous_version' },
+ });
+});
+
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper.find('.spinner')).toHaveLength(1);
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render a measure overview', async () => {
+ const wrapper = shallowRender({
+ location: mockLocation({ pathname: '/component_measures', query: { metric: 'Reliability' } }),
+ });
+ expect(wrapper.find('.spinner')).toHaveLength(1);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('MeasureOverviewContainer')).toHaveLength(1);
+});
+
+it('should render a message when there are no measures', async () => {
+ (getMeasuresWithPeriod as jest.Mock).mockResolvedValue({
+ component: { measures: [] },
+ period: { mode: 'previous_version' },
+ });
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should not render drilldown for estimated duplications', async () => {
+ const wrapper = shallowRender({ branchLike: mockPullRequest({ title: '' }) });
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should refresh branch status if issues are updated', async () => {
+ const fetchBranchStatus = jest.fn();
+ const branchLike = mockPullRequest();
+ const wrapper = shallowRender({ branchLike, fetchBranchStatus });
+ const instance = wrapper.instance();
+ await waitAndUpdate(wrapper);
+
+ instance.handleIssueChange(mockIssue());
+ expect(fetchBranchStatus).toHaveBeenCalledWith(branchLike, 'foo');
+});
+
+it('should render a warning message when user does not have access to all projects whithin a Portfolio', async () => {
+ const wrapper = shallowRender({
+ component: mockComponent({
+ qualifier: ComponentQualifier.Portfolio,
+ canBrowseAllChildProjects: false,
+ }),
+ });
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot(
+ 'Measure menu with warning (ScreenPositionHelper)'
+ );
+});
+
+it.each([
+ [ComponentQualifier.Portfolio, true, false],
+ [ComponentQualifier.Project, false, false],
+ [ComponentQualifier.Portfolio, false, true],
+])(
+ 'should not render a warning message',
+ async (
+ componentQualifier: ComponentQualifier,
+ canBrowseAllChildProjects: boolean,
+ alertIsVisible: boolean
+ ) => {
+ const wrapper = shallowRender({
+ component: mockComponent({
+ qualifier: componentQualifier,
+ canBrowseAllChildProjects,
+ }),
+ });
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find(ScreenPositionHelper).dive().find(Alert).exists()).toBe(alertIsVisible);
+ }
+);
+
+function shallowRender(props: Partial<ComponentMeasuresApp['props']> = {}) {
+ return shallow<ComponentMeasuresApp>(
+ <ComponentMeasuresApp
+ branchLike={mockMainBranch()}
+ component={mockComponent({ key: 'foo', name: 'Foo' })}
+ fetchBranchStatus={jest.fn()}
+ location={mockLocation({ pathname: '/component_measures', query: { metric: 'coverage' } })}
+ router={mockRouter()}
+ {...props}
+ />
+ );
+}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not render drilldown for estimated duplications 1`] = `
-<div
- id="component-measures"
->
- <Suggestions
- suggestions="component_measures"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="Coverage"
- />
- <div
- className="layout-page"
- >
- <ScreenPositionHelper
- className="layout-page-side-outer"
- >
- <Component />
- </ScreenPositionHelper>
- <main
- className="layout-page-main"
- >
- <div
- className="layout-page-main-inner"
- >
- <div
- className="note"
- >
- component_measures.details_are_not_available
- </div>
- </div>
- </main>
- </div>
-</div>
-`;
-
-exports[`should render a message when there are no measures 1`] = `
-<div
- id="component-measures"
->
- <Suggestions
- suggestions="component_measures"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="Coverage"
- />
- <MeasuresEmpty />
-</div>
-`;
-
-exports[`should render a warning message when user does not have access to all projects whithin a Portfolio: Measure menu with warning (ScreenPositionHelper) 1`] = `
-<div
- className="layout-page-side-outer"
->
- <div
- className="layout-page-side"
- style={
- {
- "top": 0,
- }
- }
- >
- <div
- className="layout-page-side-inner"
- >
- <Alert
- className="big-spacer-top big-spacer-right big-spacer-left it__portfolio_warning"
- variant="warning"
- >
- <Styled(div)>
- component_measures.not_all_measures_are_shown
- <HelpTooltip
- ariaLabel="component_measures.not_all_measures_are_shown.help"
- className="spacer-left"
- overlay="component_measures.not_all_measures_are_shown.help"
- />
- </Styled(div)>
- </Alert>
- <div
- className="layout-page-filters"
- >
- <Sidebar
- measures={
- [
- {
- "leak": undefined,
- "metric": {
- "domain": "Coverage",
- "id": "2",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- "value": "80.0",
- },
- ]
- }
- selectedMetric="coverage"
- showFullMeasures={true}
- updateQuery={[Function]}
- />
- </div>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correctly 1`] = `
-<div
- id="component-measures"
->
- <Suggestions
- suggestions="component_measures"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="Coverage"
- />
- <div
- className="layout-page"
- >
- <ScreenPositionHelper
- className="layout-page-side-outer"
- >
- <Component />
- </ScreenPositionHelper>
- <MeasureContent
- branchLike={
- {
- "analysisDate": "2018-01-01",
- "excludedFromPurge": true,
- "isMain": true,
- "name": "master",
- }
- }
- metrics={
- {
- "coverage": {
- "domain": "Coverage",
- "id": "2",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- },
- "duplicated_lines_density": {
- "domain": "Duplications",
- "id": "3",
- "key": "duplicated_lines_density",
- "name": "Duplicated Lines (%)",
- "type": "PERCENT",
- },
- "lines_to_cover": {
- "domain": "Coverage",
- "id": "1",
- "key": "lines_to_cover",
- "name": "Lines to Cover",
- "type": "INT",
- },
- "new_bugs": {
- "domain": "Reliability",
- "id": "4",
- "key": "new_bugs",
- "name": "New Bugs",
- "type": "INT",
- },
- }
- }
- onIssueChange={[Function]}
- requestedMetric={
- {
- "domain": "Coverage",
- "id": "2",
- "key": "coverage",
- "name": "Coverage",
- "type": "PERCENT",
- }
- }
- rootComponent={
- {
- "breadcrumbs": [],
- "key": "foo",
- "name": "Foo",
- "qualifier": "TRK",
- "qualityGate": {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": [
- {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": [],
- }
- }
- router={
- {
- "createHref": [MockFunction],
- "createPath": [MockFunction],
- "go": [MockFunction],
- "goBack": [MockFunction],
- "goForward": [MockFunction],
- "isActive": [MockFunction],
- "push": [MockFunction],
- "replace": [MockFunction],
- "setRouteLeaveHook": [MockFunction],
- }
- }
- selected=""
- updateQuery={[Function]}
- view="tree"
- />
- </div>
-</div>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render drilldown for estimated duplications 1`] = `
+<div
+ id="component-measures"
+>
+ <Suggestions
+ suggestions="component_measures"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ prioritizeSeoTags={false}
+ title="layout.measures"
+ />
+ <div
+ className="layout-page"
+ >
+ <ScreenPositionHelper
+ className="layout-page-side-outer"
+ >
+ <Component />
+ </ScreenPositionHelper>
+ <main
+ className="layout-page-main"
+ >
+ <div
+ className="layout-page-main-inner"
+ >
+ <div
+ className="note"
+ >
+ component_measures.details_are_not_available
+ </div>
+ </div>
+ </main>
+ </div>
+</div>
+`;
+
+exports[`should render a message when there are no measures 1`] = `
+<div
+ id="component-measures"
+>
+ <Suggestions
+ suggestions="component_measures"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ prioritizeSeoTags={false}
+ title="layout.measures"
+ />
+ <MeasuresEmpty />
+</div>
+`;
+
+exports[`should render a warning message when user does not have access to all projects whithin a Portfolio: Measure menu with warning (ScreenPositionHelper) 1`] = `
+<div
+ className="layout-page-side-outer"
+>
+ <div
+ className="layout-page-side"
+ style={
+ {
+ "top": 0,
+ }
+ }
+ >
+ <div
+ className="layout-page-side-inner"
+ >
+ <Alert
+ className="big-spacer-top big-spacer-right big-spacer-left it__portfolio_warning"
+ variant="warning"
+ >
+ <Styled(div)>
+ component_measures.not_all_measures_are_shown
+ <HelpTooltip
+ ariaLabel="component_measures.not_all_measures_are_shown.help"
+ className="spacer-left"
+ overlay="component_measures.not_all_measures_are_shown.help"
+ />
+ </Styled(div)>
+ </Alert>
+ <div
+ className="layout-page-filters"
+ >
+ <Sidebar
+ measures={
+ [
+ {
+ "leak": undefined,
+ "metric": {
+ "domain": "Coverage",
+ "id": "2",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ },
+ "value": "80.0",
+ },
+ ]
+ }
+ selectedMetric="coverage"
+ showFullMeasures={true}
+ updateQuery={[Function]}
+ />
+ </div>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ id="component-measures"
+>
+ <Suggestions
+ suggestions="component_measures"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ prioritizeSeoTags={false}
+ title="layout.measures"
+ />
+ <div
+ className="layout-page"
+ >
+ <ScreenPositionHelper
+ className="layout-page-side-outer"
+ >
+ <Component />
+ </ScreenPositionHelper>
+ <MeasureContent
+ branchLike={
+ {
+ "analysisDate": "2018-01-01",
+ "excludedFromPurge": true,
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ metrics={
+ {
+ "coverage": {
+ "domain": "Coverage",
+ "id": "2",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ },
+ "duplicated_lines_density": {
+ "domain": "Duplications",
+ "id": "3",
+ "key": "duplicated_lines_density",
+ "name": "Duplicated Lines (%)",
+ "type": "PERCENT",
+ },
+ "lines_to_cover": {
+ "domain": "Coverage",
+ "id": "1",
+ "key": "lines_to_cover",
+ "name": "Lines to Cover",
+ "type": "INT",
+ },
+ "new_bugs": {
+ "domain": "Reliability",
+ "id": "4",
+ "key": "new_bugs",
+ "name": "New Bugs",
+ "type": "INT",
+ },
+ }
+ }
+ onIssueChange={[Function]}
+ requestedMetric={
+ {
+ "domain": "Coverage",
+ "id": "2",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ }
+ }
+ rootComponent={
+ {
+ "breadcrumbs": [],
+ "key": "foo",
+ "name": "Foo",
+ "qualifier": "TRK",
+ "qualityGate": {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": [
+ {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": [],
+ }
+ }
+ router={
+ {
+ "createHref": [MockFunction],
+ "createPath": [MockFunction],
+ "go": [MockFunction],
+ "goBack": [MockFunction],
+ "goForward": [MockFunction],
+ "isActive": [MockFunction],
+ "push": [MockFunction],
+ "replace": [MockFunction],
+ "setRouteLeaveHook": [MockFunction],
+ }
+ }
+ selected=""
+ updateQuery={[Function]}
+ view="tree"
+ />
+ </div>
+</div>
+`;
import NavigateWithParams from '../../app/utils/NavigateWithParams';
import { omitNil } from '../../helpers/request';
import { searchParamsToQuery } from '../../helpers/urls';
-import App from './components/App';
+import ComponentMeasuresApp from './components/ComponentMeasuresApp';
const routes = () => (
<Route path="component_measures">
- <Route index={true} element={<App />} />
+ <Route index={true} element={<ComponentMeasuresApp />} />
<Route
path="domain/:domainName"
element={
id="issues-page"
>
<Suggestions suggestions="issues" />
- <Helmet defer={false} title={openIssue ? openIssue.message : translate('issues.page')} />
+ {openIssue ? (
+ <Helmet
+ defer={false}
+ title={openIssue.message}
+ titleTemplate={translateWithParameters(
+ 'page_title.template.with_category',
+ translate('issues.page')
+ )}
+ />
+ ) : (
+ <Helmet defer={false} title={translate('issues.page')} />
+ )}
<h1 className="a11y-hidden">{translate('issues.page')}</h1>
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import '../../../components/search-navigator.css';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import {
addSideBarClass,
addWhitePageClass,
render() {
const { name } = this.props;
const { canCreate, qualityGates } = this.state;
- const defaultTitle = translate('quality_gates.page');
return (
<>
- <Helmet defaultTitle={defaultTitle} defer={false} titleTemplate={`%s - ${defaultTitle}`} />
+ <Helmet
+ defer={false}
+ titleTemplate={translateWithParameters(
+ 'page_title.template.with_category',
+ translate('quality_gates.page')
+ )}
+ />
<div className="layout-page" id="quality-gates-page">
<Suggestions suggestions="quality_gates" />
import { Helmet } from 'react-helmet-async';
import { Outlet, useSearchParams } from 'react-router-dom';
import { useLocation } from '../../../components/hoc/withRouter';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import ProfileHeader from '../details/ProfileHeader';
import { useQualityProfilesContext } from '../qualityProfilesContext';
import ProfileNotFound from './ProfileNotFound';
return (
<div id="quality-profile">
- <Helmet defer={false} title={profile.name} />
+ <Helmet
+ defer={false}
+ title={profile.name}
+ titleTemplate={translateWithParameters(
+ 'page_title.template.with_category',
+ translate('quality_profiles.page')
+ )}
+ />
<ProfileHeader
profile={profile}
isComparable={filteredProfiles.length > 1}
#
#------------------------------------------------------------------------------
+page_title.template.default=%s - SonarQube
+page_title.template.with_category=%s - {0} - SonarQube
overview.page=Overview
code.page=Code
permissions.page=Permissions
#------------------------------------------------------------------------------
quality_profiles.page_title_changelog_x={0} - Quality profile changelog
-quality_profiles.page_title_compare_x={0} - Quality profile comparaison
+quality_profiles.page_title_compare_x={0} - Quality profile comparison
quality_profiles.new_profile=New Quality Profile
quality_profiles.compare_with=Compare with
quality_profiles.filter_by=Filter profiles by