aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/design-system/src/components/input/Checkbox.tsx24
-rw-r--r--server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts3
-rw-r--r--server/sonar-web/src/main/js/api/quality-profiles.ts11
-rw-r--r--server/sonar-web/src/main/js/api/rules.ts7
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts118
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts154
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormFieldsCCT.tsx223
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx121
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/shared/SoftwareImpactPillList.tsx13
-rw-r--r--server/sonar-web/src/main/js/helpers/constants.ts26
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts2
-rw-r--r--server/sonar-web/src/main/js/types/clean-code-taxonomy.ts5
-rw-r--r--server/sonar-web/src/main/js/types/issues.ts8
-rw-r--r--server/sonar-web/src/main/js/types/types.ts14
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties3
17 files changed, 519 insertions, 245 deletions
diff --git a/server/sonar-web/design-system/src/components/input/Checkbox.tsx b/server/sonar-web/design-system/src/components/input/Checkbox.tsx
index f4c917c51cf..b6dc56e99fc 100644
--- a/server/sonar-web/design-system/src/components/input/Checkbox.tsx
+++ b/server/sonar-web/design-system/src/components/input/Checkbox.tsx
@@ -75,13 +75,11 @@ export function Checkbox({
onFocus={onFocus}
type="checkbox"
/>
- <div>
- <Spinner loading={loading}>
- <StyledCheckbox aria-hidden data-clickable="true" title={title}>
- <CheckboxIcon checked={checked} thirdState={thirdState} />
- </StyledCheckbox>
- </Spinner>
- </div>
+ <Spinner loading={loading}>
+ <StyledCheckbox aria-hidden data-clickable="true" title={title}>
+ <CheckboxIcon checked={checked} thirdState={thirdState} />
+ </StyledCheckbox>
+ </Spinner>
{!right && children}
</CheckboxContainer>
);
@@ -145,33 +143,33 @@ export const AccessibleCheckbox = styled.input`
&:focus,
&:active {
- &:not(:disabled) + div > ${StyledCheckbox} {
+ &:not(:disabled) ~ ${StyledCheckbox} {
outline: ${themeBorder('focus', 'primary')};
}
}
&:checked {
- & + div > ${StyledCheckbox} {
+ & ~ ${StyledCheckbox} {
background: ${themeColor('primary')};
}
- &:disabled + div > ${StyledCheckbox} {
+ &:disabled ~ ${StyledCheckbox} {
background: ${themeColor('checkboxDisabledChecked')};
}
}
&:hover {
- &:not(:disabled) + div > ${StyledCheckbox} {
+ &:not(:disabled) ~ ${StyledCheckbox} {
background: ${themeColor('checkboxHover')};
border: ${themeBorder('default', 'primary')};
}
- &:checked:not(:disabled) + div > ${StyledCheckbox} {
+ &:checked:not(:disabled) ~ ${StyledCheckbox} {
background: ${themeColor('checkboxCheckedHover')};
border: ${themeBorder('default', 'checkboxCheckedHover')};
}
}
- &:disabled + div > ${StyledCheckbox} {
+ &:disabled ~ ${StyledCheckbox} {
background: ${themeColor('checkboxDisabled')};
color: ${themeColor('checkboxDisabled')};
border: ${themeBorder('default', 'checkboxDisabledChecked')};
diff --git a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
index 28bbeb45838..e7f50d93529 100644
--- a/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
@@ -357,7 +357,6 @@ export default class CodingRulesServiceMock {
: rule.remFnBaseEffort;
rule.remFnType =
data.remediation_fn_type !== undefined ? data.remediation_fn_type : rule.remFnType;
- rule.severity = data.severity !== undefined ? data.severity : rule.severity;
rule.status = data.status !== undefined ? data.status : rule.status;
rule.tags = data.tags !== undefined ? data.tags.split(',') : rule.tags;
@@ -369,7 +368,7 @@ export default class CodingRulesServiceMock {
descriptionSections: [
{ key: RuleDescriptionSections.DEFAULT, content: data.markdownDescription },
],
- ...pick(data, ['templateKey', 'severity', 'type', 'name', 'status']),
+ ...pick(data, ['templateKey', 'name', 'status', 'cleanCodeAttribute', 'impacts']),
key: data.customKey,
params:
data.params?.split(';').map((param: string) => {
diff --git a/server/sonar-web/src/main/js/api/quality-profiles.ts b/server/sonar-web/src/main/js/api/quality-profiles.ts
index 8f10efc1fcc..880b633d124 100644
--- a/server/sonar-web/src/main/js/api/quality-profiles.ts
+++ b/server/sonar-web/src/main/js/api/quality-profiles.ts
@@ -22,11 +22,7 @@ import { Exporter, ProfileChangelogEvent } from '../apps/quality-profiles/types'
import { csvEscape } from '../helpers/csv';
import { throwGlobalError } from '../helpers/error';
import { RequestData, getJSON, post, postJSON } from '../helpers/request';
-import {
- CleanCodeAttributeCategory,
- SoftwareImpactSeverity,
- SoftwareQuality,
-} from '../types/clean-code-taxonomy';
+import { CleanCodeAttributeCategory, SoftwareImpact } from '../types/clean-code-taxonomy';
import { Dict, Paging, ProfileInheritanceDetails, UserSelected } from '../types/types';
export interface ProfileActions {
@@ -196,10 +192,7 @@ export interface RuleCompare {
key: string;
name: string;
cleanCodeAttributeCategory?: CleanCodeAttributeCategory;
- impacts: Array<{
- softwareQuality: SoftwareQuality;
- severity: SoftwareImpactSeverity;
- }>;
+ impacts: SoftwareImpact[];
left?: { params: Dict<string>; severity: string };
right?: { params: Dict<string>; severity: string };
}
diff --git a/server/sonar-web/src/main/js/api/rules.ts b/server/sonar-web/src/main/js/api/rules.ts
index 87c1103d6e4..f8cbf1c301e 100644
--- a/server/sonar-web/src/main/js/api/rules.ts
+++ b/server/sonar-web/src/main/js/api/rules.ts
@@ -19,9 +19,10 @@
*/
import { throwGlobalError } from '../helpers/error';
import { getJSON, post, postJSON } from '../helpers/request';
+import { CleanCodeAttribute, SoftwareImpact } from '../types/clean-code-taxonomy';
import { GetRulesAppResponse, SearchRulesResponse } from '../types/coding-rules';
import { SearchRulesQuery } from '../types/rules';
-import { RuleActivation, RuleDetails, RuleType, RulesUpdateRequest } from '../types/types';
+import { RuleActivation, RuleDetails, RulesUpdateRequest } from '../types/types';
export interface CreateRuleData {
customKey: string;
@@ -29,10 +30,10 @@ export interface CreateRuleData {
name: string;
params?: string;
preventReactivation?: boolean;
- severity?: string;
status?: string;
templateKey: string;
- type?: RuleType;
+ cleanCodeAttribute: CleanCodeAttribute;
+ impacts: SoftwareImpact[];
}
export function getRulesApp(): Promise<GetRulesAppResponse> {
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
index d0c0d7726ae..f718d41646a 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
+++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
@@ -24,16 +24,13 @@ import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { QP_2, RULE_1 } from '../../../api/mocks/data/ids';
import { CLEAN_CODE_CATEGORIES, SOFTWARE_QUALITIES } from '../../../helpers/constants';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
-import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import {
CleanCodeAttribute,
CleanCodeAttributeCategory,
SoftwareQuality,
} from '../../../types/clean-code-taxonomy';
import { SettingsKey } from '../../../types/settings';
-import { CurrentUser } from '../../../types/users';
-import routes from '../routes';
-import { getPageObjects } from '../utils-tests';
+import { getPageObjects, renderCodingRulesApp } from '../utils-tests';
const rulesHandler = new CodingRulesServiceMock();
const settingsHandler = new SettingsServiceMock();
@@ -57,7 +54,7 @@ describe('Rules app list', () => {
// Render clean code attributes.
expect(
- ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Adaptable).getAll().length,
+ ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Intentional).getAll().length,
).toBeGreaterThan(1);
expect(ui.ruleSoftwareQuality(SoftwareQuality.Maintainability).getAll().length).toBeGreaterThan(
1,
@@ -175,7 +172,7 @@ describe('Rules app list', () => {
expect(ui.getAllRuleListItems()).toHaveLength(11);
// Filter by clean code category
- await user.click(ui.facetItem('issue.clean_code_attribute_category.ADAPTABLE').get());
+ await user.click(ui.facetItem('issue.clean_code_attribute_category.INTENTIONAL').get());
expect(ui.getAllRuleListItems()).toHaveLength(10);
@@ -383,7 +380,7 @@ describe('Rule app details', () => {
await ui.detailsloaded();
expect(ui.ruleTitle('Awsome java rule').get()).toBeInTheDocument();
expect(
- ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Adaptable).get(),
+ ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Intentional).get(),
).toBeInTheDocument();
expect(ui.ruleCleanCodeAttribute(CleanCodeAttribute.Clear).get()).toBeInTheDocument();
// 1 In Rule details + 1 in facet
@@ -627,96 +624,6 @@ describe('Rule app details', () => {
expect(ui.tagCheckbox(RULE_TAGS_MOCK[2]).get()).toBeInTheDocument();
expect(ui.tagCheckbox(RULE_TAGS_MOCK[1]).query()).not.toBeInTheDocument();
});
-
- describe('custom rule', () => {
- it('can create custom rule', async () => {
- const { ui, user } = getPageObjects();
- rulesHandler.setIsAdmin();
- renderCodingRulesApp(mockLoggedInUser());
- await ui.appLoaded();
-
- await user.click(ui.templateFacet.get());
- await user.click(ui.facetItem('coding_rules.filters.template.is_template').get());
-
- // Shows only one template rule
- expect(ui.getAllRuleListItems()).toHaveLength(1);
-
- // Show template rule details
- await user.click(ui.ruleListItemLink('Template rule').get());
- expect(ui.ruleTitle('Template rule').get()).toBeInTheDocument();
- expect(ui.customRuleSectionTitle.get()).toBeInTheDocument();
-
- // Create custom rule
- await user.click(ui.createCustomRuleButton.get());
- await user.type(ui.ruleNameTextbox.get(), 'New Custom Rule');
- expect(ui.keyTextbox.get()).toHaveValue('New_Custom_Rule');
- await user.clear(ui.keyTextbox.get());
- await user.type(ui.keyTextbox.get(), 'new_custom_rule');
-
- await selectEvent.select(ui.typeSelect.get(), 'issue.type.BUG');
- await selectEvent.select(ui.oldSeveritySelect.get(), 'severity.MINOR');
- await selectEvent.select(ui.statusSelect.get(), 'rules.status.BETA');
-
- await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
- await user.type(ui.paramInput('1').get(), 'Default value');
-
- await user.click(ui.createButton.get());
-
- // Verify the rule is created
- expect(ui.customRuleItemLink('New Custom Rule').get()).toBeInTheDocument();
- });
-
- it('can edit custom rule', async () => {
- const { ui, user } = getPageObjects();
- rulesHandler.setIsAdmin();
- renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
- await ui.detailsloaded();
-
- await user.click(ui.editCustomRuleButton.get());
-
- // Change name and description of custom rule
- await user.clear(ui.ruleNameTextbox.get());
- await user.type(ui.ruleNameTextbox.get(), 'Updated custom rule name');
- await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
-
- await user.click(ui.saveButton.get(ui.updateCustomRuleDialog.get()));
-
- expect(ui.ruleTitle('Updated custom rule name').get()).toBeInTheDocument();
- });
-
- it('can delete custom rule', async () => {
- const { ui, user } = getPageObjects();
- rulesHandler.setIsAdmin();
- renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
- await ui.detailsloaded();
-
- await user.click(ui.deleteButton.get());
- await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
-
- // Shows the list of rules, custom rule should not be included
- expect(ui.ruleListItemLink('Custom Rule based on rule8').query()).not.toBeInTheDocument();
- });
-
- it('can delete custom rule from template page', async () => {
- const { ui, user } = getPageObjects();
- rulesHandler.setIsAdmin();
- renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
- await ui.detailsloaded();
-
- await user.click(ui.deleteCustomRuleButton('Custom Rule based on rule8').get());
- await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
- expect(ui.customRuleItemLink('Custom Rule based on rule8').query()).not.toBeInTheDocument();
- });
-
- it('anonymous user cannot modify custom rule', async () => {
- const { ui } = getPageObjects();
- renderCodingRulesApp(undefined, 'coding_rules?open=rule9');
- await ui.appLoaded();
-
- expect(ui.editCustomRuleButton.query()).not.toBeInTheDocument();
- expect(ui.deleteButton.query()).not.toBeInTheDocument();
- });
- });
});
describe('redirects', () => {
@@ -733,9 +640,9 @@ describe('redirects', () => {
renderCodingRulesApp(
mockLoggedInUser(),
- 'coding_rules#languages=c,js|types=BUG|cleanCodeAttributeCategories=ADAPTABLE',
+ 'coding_rules#languages=c,js|types=BUG|cleanCodeAttributeCategories=INTENTIONAL',
);
- expect(ui.facetItem('issue.clean_code_attribute_category.ADAPTABLE').get()).toBeChecked();
+ expect(ui.facetItem('issue.clean_code_attribute_category.INTENTIONAL').get()).toBeChecked();
await user.click(ui.typeFacet.get());
expect(await ui.facetItem(/issue.type.BUG/).find()).toBeChecked();
@@ -744,16 +651,3 @@ describe('redirects', () => {
expect(screen.getByText('x_of_y_shown.2.2')).toBeInTheDocument();
});
});
-
-function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) {
- renderAppRoutes('coding_rules', routes, {
- navigateTo,
- currentUser,
- languages: {
- js: { key: 'js', name: 'JavaScript' },
- java: { key: 'java', name: 'Java' },
- c: { key: 'c', name: 'C' },
- py: { key: 'py', name: 'Python' },
- },
- });
-}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts
new file mode 100644
index 00000000000..850c4314fed
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts
@@ -0,0 +1,154 @@
+/*
+ * 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 selectEvent from 'react-select-event';
+import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
+import { mockLoggedInUser } from '../../../helpers/testMocks';
+import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
+import { getPageObjects, renderCodingRulesApp } from '../utils-tests';
+
+const rulesHandler = new CodingRulesServiceMock();
+const settingsHandler = new SettingsServiceMock();
+
+afterEach(() => {
+ rulesHandler.reset();
+ settingsHandler.reset();
+});
+
+describe('custom rule', () => {
+ it('can create custom rule', async () => {
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser());
+ await ui.appLoaded();
+
+ await user.click(ui.templateFacet.get());
+ await user.click(ui.facetItem('coding_rules.filters.template.is_template').get());
+
+ // Shows only one template rule
+ expect(ui.getAllRuleListItems()).toHaveLength(1);
+
+ // Show template rule details
+ await user.click(ui.ruleListItemLink('Template rule').get());
+ expect(ui.ruleTitle('Template rule').get()).toBeInTheDocument();
+ expect(ui.customRuleSectionTitle.get()).toBeInTheDocument();
+
+ // Create custom rule
+ await user.click(ui.createCustomRuleButton.get());
+ await user.type(ui.ruleNameTextbox.get(), 'New Custom Rule');
+ expect(ui.keyTextbox.get()).toHaveValue('New_Custom_Rule');
+ await user.clear(ui.keyTextbox.get());
+ await user.type(ui.keyTextbox.get(), 'new_custom_rule');
+
+ await selectEvent.select(
+ ui.cleanCodeCategorySelect.get(),
+ 'rule.clean_code_attribute_category.CONSISTENT',
+ );
+ await selectEvent.select(
+ ui.cleanCodeAttributeSelect.get(),
+ 'rule.clean_code_attribute.IDENTIFIABLE',
+ );
+
+ await selectEvent.select(
+ ui.cleanCodeCategorySelect.get(),
+ 'rule.clean_code_attribute_category.INTENTIONAL',
+ );
+ // Setting default clean code category of a template should set corresponding attribute
+ expect(
+ ui.createCustomRuleDialog.byText('rule.clean_code_attribute.CLEAR').get(),
+ ).toBeInTheDocument();
+
+ // Set software qualities
+ expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Maintainability).get()).toBeChecked();
+ // Uncheck all software qualities - should see error message
+ await user.click(ui.cleanCodeQualityCheckbox(SoftwareQuality.Maintainability).get());
+ expect(
+ ui.createCustomRuleDialog.byText('coding_rules.custom_rule.select_software_quality').get(),
+ ).toBeInTheDocument();
+
+ await user.click(ui.cleanCodeQualityCheckbox(SoftwareQuality.Reliability).get());
+ await selectEvent.select(
+ ui.cleanCodeSeveritySelect(SoftwareQuality.Reliability).get(),
+ 'severity.MEDIUM',
+ );
+ expect(ui.createCustomRuleDialog.byText('severity.MEDIUM').get()).toBeInTheDocument();
+
+ await selectEvent.select(ui.statusSelect.get(), 'rules.status.BETA');
+
+ await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
+ await user.type(ui.paramInput('1').get(), 'Default value');
+
+ await user.click(ui.createButton.get());
+
+ // Verify the rule is created
+ expect(ui.customRuleItemLink('New Custom Rule').get()).toBeInTheDocument();
+ });
+
+ it('can edit custom rule', async () => {
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
+ await ui.detailsloaded();
+
+ await user.click(ui.editCustomRuleButton.get());
+
+ // Change name and description of custom rule
+ await user.clear(ui.ruleNameTextbox.get());
+ await user.type(ui.ruleNameTextbox.get(), 'Updated custom rule name');
+ await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
+
+ await user.click(ui.saveButton.get(ui.updateCustomRuleDialog.get()));
+
+ expect(ui.ruleTitle('Updated custom rule name').get()).toBeInTheDocument();
+ });
+
+ it('can delete custom rule', async () => {
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
+ await ui.detailsloaded();
+
+ await user.click(ui.deleteButton.get());
+ await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
+
+ // Shows the list of rules, custom rule should not be included
+ expect(ui.ruleListItemLink('Custom Rule based on rule8').query()).not.toBeInTheDocument();
+ });
+
+ it('can delete custom rule from template page', async () => {
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+ await ui.detailsloaded();
+
+ await user.click(ui.deleteCustomRuleButton('Custom Rule based on rule8').get());
+ await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
+ expect(ui.customRuleItemLink('Custom Rule based on rule8').query()).not.toBeInTheDocument();
+ });
+
+ it('anonymous user cannot modify custom rule', async () => {
+ const { ui } = getPageObjects();
+ renderCodingRulesApp(undefined, 'coding_rules?open=rule9');
+ await ui.appLoaded();
+
+ expect(ui.editCustomRuleButton.query()).not.toBeInTheDocument();
+ expect(ui.deleteButton.query()).not.toBeInTheDocument();
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormFieldsCCT.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormFieldsCCT.tsx
new file mode 100644
index 00000000000..63be3ec9962
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormFieldsCCT.tsx
@@ -0,0 +1,223 @@
+/*
+ * 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 {
+ Checkbox,
+ FormField,
+ Highlight,
+ InputSelect,
+ LightPrimary,
+ RequiredIcon,
+ TextError,
+} from 'design-system';
+import React from 'react';
+import { useIntl } from 'react-intl';
+import SoftwareImpactSeverityIcon from '../../../components/icons/SoftwareImpactSeverityIcon';
+import {
+ CLEAN_CODE_ATTRIBUTES_BY_CATEGORY,
+ CLEAN_CODE_CATEGORIES,
+ IMPACT_SEVERITIES,
+ SOFTWARE_QUALITIES,
+} from '../../../helpers/constants';
+import {
+ CleanCodeAttribute,
+ CleanCodeAttributeCategory,
+ SoftwareImpact,
+ SoftwareImpactSeverity,
+ SoftwareQuality,
+} from '../../../types/clean-code-taxonomy';
+
+interface Props<T> {
+ value: T;
+ onChange: (value: T) => void;
+ disabled?: boolean;
+}
+
+export function CleanCodeCategoryField(props: Readonly<Props<CleanCodeAttributeCategory>>) {
+ const { value, disabled } = props;
+ const intl = useIntl();
+
+ const categories = CLEAN_CODE_CATEGORIES.map((category) => ({
+ value: category,
+ label: intl.formatMessage({ id: `rule.clean_code_attribute_category.${category}` }),
+ }));
+
+ return (
+ <FormField
+ ariaLabel={intl.formatMessage({ id: 'category' })}
+ label={intl.formatMessage({ id: 'category' })}
+ htmlFor="coding-rules-custom-clean-code-category"
+ >
+ <InputSelect
+ options={categories}
+ inputId="coding-rules-custom-clean-code-category"
+ onChange={(option) => props.onChange(option?.value as CleanCodeAttributeCategory)}
+ isClearable={false}
+ isDisabled={disabled}
+ isSearchable={false}
+ value={categories.find((category) => category.value === value)}
+ />
+ </FormField>
+ );
+}
+
+export function CleanCodeAttributeField(
+ props: Readonly<Props<CleanCodeAttribute> & { category: CleanCodeAttributeCategory }>,
+) {
+ const { value, disabled, category, onChange } = props;
+ const initialAttribute = React.useRef(value);
+ const intl = useIntl();
+
+ const attributes = CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].map((attribute) => ({
+ value: attribute,
+ label: intl.formatMessage({ id: `rule.clean_code_attribute.${attribute}` }),
+ }));
+
+ // Set default CC attribute when category changes
+ React.useEffect(() => {
+ if (CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].includes(value)) {
+ return;
+ }
+ const initialAttributeIndex = CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].findIndex(
+ (attribute) => attribute === initialAttribute.current,
+ );
+ onChange(
+ CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category][
+ initialAttributeIndex === -1 ? 0 : initialAttributeIndex
+ ],
+ );
+ }, [onChange, category, value]);
+
+ return (
+ <FormField
+ ariaLabel={intl.formatMessage({ id: 'attribute' })}
+ label={intl.formatMessage({ id: 'attribute' })}
+ htmlFor="coding-rules-custom-clean-code-attribute"
+ >
+ <InputSelect
+ options={attributes}
+ inputId="coding-rules-custom-clean-code-attribute"
+ onChange={(option) => props.onChange(option?.value as CleanCodeAttribute)}
+ isClearable={false}
+ isDisabled={disabled}
+ isSearchable={false}
+ value={attributes.find((attribute) => attribute.value === value)}
+ />
+ </FormField>
+ );
+}
+
+export function SoftwareQualitiesFields(
+ props: Readonly<Props<SoftwareImpact[]> & { error: boolean }>,
+) {
+ const { value, disabled, error } = props;
+ const intl = useIntl();
+
+ const severities = React.useMemo(
+ () =>
+ IMPACT_SEVERITIES.map((severity) => ({
+ value: severity,
+ label: intl.formatMessage({ id: `severity.${severity}` }),
+ Icon: <SoftwareImpactSeverityIcon severity={severity} />,
+ })),
+ [intl],
+ );
+
+ const handleSoftwareQualityChange = (quality: SoftwareQuality, checked: boolean) => {
+ if (checked) {
+ props.onChange([
+ ...value,
+ { softwareQuality: quality, severity: SoftwareImpactSeverity.Low },
+ ]);
+ } else {
+ props.onChange(value.filter((impact) => impact.softwareQuality !== quality));
+ }
+ };
+
+ const handleSeverityChange = (quality: SoftwareQuality, severity: SoftwareImpactSeverity) => {
+ props.onChange(
+ value.map((impact) =>
+ impact.softwareQuality === quality ? { ...impact, severity } : impact,
+ ),
+ );
+ };
+
+ return (
+ <fieldset className="sw-mt-2 sw-mb-4 sw-relative">
+ <legend className="sw-w-full sw-flex sw-justify-between sw-gap-6 sw-mb-4">
+ <Highlight className="sw-w-full">
+ {intl.formatMessage({ id: 'software_quality' })}
+ <RequiredIcon aria-label={intl.formatMessage({ id: 'required' })} className="sw-ml-1" />
+ </Highlight>
+ <Highlight className="sw-w-full">
+ {intl.formatMessage({ id: 'severity' })}
+ <RequiredIcon aria-label={intl.formatMessage({ id: 'required' })} className="sw-ml-1" />
+ </Highlight>
+ </legend>
+ {SOFTWARE_QUALITIES.map((quality) => {
+ const selectedQuality = value.find((impact) => impact.softwareQuality === quality);
+ const selectedSeverity = selectedQuality
+ ? severities.find((severity) => severity.value === selectedQuality.severity)
+ : null;
+
+ return (
+ <fieldset key={quality} className="sw-flex sw-justify-between sw-gap-6 sw-mb-4">
+ <legend className="sw-sr-only">
+ {intl.formatMessage(
+ { id: 'coding_rules.custom_rule.software_quality_x' },
+ { quality },
+ )}
+ </legend>
+ <Checkbox
+ className="sw-w-full"
+ checked={Boolean(selectedQuality)}
+ onCheck={(checked) => {
+ handleSoftwareQualityChange(quality, checked);
+ }}
+ label={quality}
+ >
+ <LightPrimary className="sw-ml-3">
+ {intl.formatMessage({ id: `software_quality.${quality}` })}
+ </LightPrimary>
+ </Checkbox>
+ <InputSelect
+ aria-label={intl.formatMessage({ id: 'severity' })}
+ className="sw-w-full"
+ options={severities}
+ placeholder={intl.formatMessage({ id: 'none' })}
+ onChange={(option) =>
+ handleSeverityChange(quality, option?.value as SoftwareImpactSeverity)
+ }
+ isClearable={false}
+ isDisabled={disabled || !selectedQuality}
+ isSearchable={false}
+ value={selectedSeverity}
+ />
+ </fieldset>
+ );
+ })}
+ {error && (
+ <TextError
+ className="sw-font-regular sw-absolute sw--bottom-3"
+ text={intl.formatMessage({ id: 'coding_rules.custom_rule.select_software_quality' })}
+ />
+ )}
+ </fieldset>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
index a8c41fc8fd6..de13a247820 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
@@ -30,18 +30,25 @@ import {
Modal,
} from 'design-system';
import * as React from 'react';
-import { OptionProps, SingleValueProps, components } from 'react-select';
import FormattingTips from '../../../components/common/FormattingTips';
-import TypeHelper from '../../../components/shared/TypeHelper';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { RULE_STATUSES, RULE_TYPES } from '../../../helpers/constants';
+import { RULE_STATUSES } from '../../../helpers/constants';
import { csvEscape } from '../../../helpers/csv';
import { translate } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
import { latinize } from '../../../helpers/strings';
import { useCreateRuleMutation, useUpdateRuleMutation } from '../../../queries/rules';
-import { Dict, RuleDetails, RuleParameter, RuleType, Status } from '../../../types/types';
-import { SeveritySelect } from './SeveritySelect';
+import {
+ CleanCodeAttribute,
+ CleanCodeAttributeCategory,
+ SoftwareImpact,
+} from '../../../types/clean-code-taxonomy';
+import { Dict, RuleDetails, RuleParameter, Status } from '../../../types/types';
+import {
+ CleanCodeAttributeField,
+ CleanCodeCategoryField,
+ SoftwareQualitiesFields,
+} from './CustomRuleFormFieldsCCT';
interface Props {
customRule?: RuleDetails;
@@ -59,9 +66,14 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
const [name, setName] = React.useState(customRule?.name ?? '');
const [params, setParams] = React.useState(getParams(customRule));
const [reactivating, setReactivating] = React.useState(false);
- const [severity, setSeverity] = React.useState(customRule?.severity ?? templateRule.severity);
const [status, setStatus] = React.useState(customRule?.status ?? templateRule.status);
- const [type, setType] = React.useState(customRule?.type ?? templateRule.type);
+ const [ccCategory, setCCCategory] = React.useState<CleanCodeAttributeCategory>(
+ templateRule.cleanCodeAttributeCategory ?? CleanCodeAttributeCategory.Consistent,
+ );
+ const [ccAttribute, setCCAtribute] = React.useState<CleanCodeAttribute>(
+ templateRule.cleanCodeAttribute ?? CleanCodeAttribute.Conventional,
+ );
+ const [impacts, setImpacts] = React.useState<SoftwareImpact[]>(templateRule?.impacts ?? []);
const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(props.onClose);
const { mutate: createRule, isLoading: creatingRule } = useCreateRuleMutation(
{
@@ -75,6 +87,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
);
const submitting = updatingRule || creatingRule;
+ const hasError = impacts.length === 0;
const submit = () => {
const stringifiedParams = Object.keys(params)
@@ -84,7 +97,6 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
markdownDescription: description,
name,
params: stringifiedParams,
- severity,
status,
};
return customRule
@@ -94,7 +106,8 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
customKey: key,
preventReactivation: !reactivating,
templateKey: templateRule.key,
- type,
+ cleanCodeAttribute: ccAttribute,
+ impacts,
});
};
@@ -180,51 +193,6 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
[description, submitting],
);
- const TypeField = React.useMemo(() => {
- const ruleTypeOption: LabelValueSelectOption<RuleType>[] = RULE_TYPES.map((type) => ({
- label: translate('issue.type', type),
- value: type,
- }));
- return (
- <FormField
- ariaLabel={translate('type')}
- label={translate('type')}
- htmlFor="coding-rules-custom-rule-type"
- >
- <InputSelect
- inputId="coding-rules-custom-rule-type"
- isClearable={false}
- isDisabled={submitting}
- isSearchable={false}
- onChange={({ value }: LabelValueSelectOption<RuleType>) => setType(value)}
- components={{
- Option: TypeSelectOption,
- SingleValue: TypeSelectValue,
- }}
- options={ruleTypeOption}
- value={ruleTypeOption.find((t) => t.value === type)}
- />
- </FormField>
- );
- }, [type, submitting]);
-
- const SeverityField = React.useMemo(
- () => (
- <FormField
- ariaLabel={translate('severity')}
- label={translate('severity')}
- htmlFor="coding-rules-severity-select"
- >
- <SeveritySelect
- isDisabled={submitting}
- onChange={({ value }: { value: string }) => setSeverity(value)}
- severity={severity}
- />
- </FormField>
- ),
- [severity, submitting],
- );
-
const StatusField = React.useMemo(() => {
const statusesOptions = RULE_STATUSES.map((status) => ({
label: translate('rules.status', status),
@@ -339,16 +307,33 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
{NameField}
{KeyField}
- {/* do not allow to change the type of existing rule */}
- {!customRule && TypeField}
- {SeverityField}
+
+ <div className="sw-flex sw-justify-between sw-gap-6">
+ <CleanCodeCategoryField
+ value={ccCategory}
+ disabled={submitting}
+ onChange={setCCCategory}
+ />
+ <CleanCodeAttributeField
+ value={ccAttribute}
+ category={ccCategory}
+ disabled={submitting}
+ onChange={setCCAtribute}
+ />
+ </div>
+ <SoftwareQualitiesFields
+ error={hasError}
+ value={impacts}
+ onChange={setImpacts}
+ disabled={submitting}
+ />
{StatusField}
{DescriptionField}
{templateParams.map(renderParameterField)}
</form>
}
primaryButton={
- <ButtonPrimary disabled={submitting} type="submit" form={FORM_ID}>
+ <ButtonPrimary disabled={submitting || hasError} type="submit" form={FORM_ID}>
{buttonText}
</ButtonPrimary>
}
@@ -358,26 +343,6 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
);
}
-function TypeSelectOption(
- optionProps: Readonly<OptionProps<LabelValueSelectOption<RuleType>, false>>,
-) {
- return (
- <components.Option {...optionProps}>
- <TypeHelper type={optionProps.data.value} />
- </components.Option>
- );
-}
-
-function TypeSelectValue(
- valueProps: Readonly<SingleValueProps<LabelValueSelectOption<RuleType>, false>>,
-) {
- return (
- <components.SingleValue {...valueProps}>
- <TypeHelper className="display-flex-center" type={valueProps.data.value} />
- </components.SingleValue>
- );
-}
-
function getParams(customRule?: RuleDetails) {
const params: Dict<string> = {};
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
index 734c2155efc..1bb89ffafb7 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
@@ -20,6 +20,7 @@
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Profile } from '../../api/quality-profiles';
+import { renderAppRoutes } from '../../helpers/testReactTestingUtils';
import {
byLabelText,
byPlaceholderText,
@@ -32,6 +33,8 @@ import {
CleanCodeAttributeCategory,
SoftwareQuality,
} from '../../types/clean-code-taxonomy';
+import { CurrentUser } from '../../types/users';
+import routes from './routes';
const selectors = {
loading: byLabelText('loading'),
@@ -174,7 +177,17 @@ const selectors = {
deleteCustomRuleDialog: byRole('dialog', { name: 'coding_rules.delete_rule' }),
ruleNameTextbox: byRole('textbox', { name: 'name' }),
keyTextbox: byRole('textbox', { name: 'key' }),
- typeSelect: byRole('combobox', { name: 'type' }),
+ cleanCodeCategorySelect: byRole('combobox', { name: 'category' }),
+ cleanCodeAttributeSelect: byRole('combobox', { name: 'attribute' }),
+ cleanCodeQualityCheckbox: (quality: SoftwareQuality) =>
+ byRole('group', { name: `coding_rules.custom_rule.software_quality_x.${quality}` }).byRole(
+ 'checkbox',
+ ),
+ cleanCodeSeveritySelect: (quality: SoftwareQuality) =>
+ byRole('group', { name: `coding_rules.custom_rule.software_quality_x.${quality}` }).byRole(
+ 'combobox',
+ { name: 'severity' },
+ ),
statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
descriptionTextbox: byRole('textbox', { name: 'description' }),
createButton: byRole('button', { name: 'create' }),
@@ -229,3 +242,16 @@ export function getPageObjects() {
user,
};
}
+
+export function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) {
+ renderAppRoutes('coding_rules', routes, {
+ navigateTo,
+ currentUser,
+ languages: {
+ js: { key: 'js', name: 'JavaScript' },
+ java: { key: 'java', name: 'Java' },
+ c: { key: 'c', name: 'C' },
+ py: { key: 'py', name: 'Python' },
+ },
+ });
+}
diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx
index 6df26597fed..6bc9ba2d3cd 100644
--- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiSidebar.tsx
@@ -104,9 +104,9 @@ export default function ApiSidebar({ apisList, docInfo }: Readonly<Props>) {
value={search}
/>
- <div className="sw-mt-4">
+ <div className="sw-mt-4 sw-flex sw-items-center">
<Checkbox checked={showInternal} onCheck={() => setShowInternal((prev) => !prev)}>
- <span className="sw-ml-2 sw-mb-1">{translate('api_documentation.show_internal')}</span>
+ <span className="sw-ml-2">{translate('api_documentation.show_internal')}</span>
</Checkbox>
<HelpTooltip className="sw-ml-2" overlay={translate('api_documentation.internal_tooltip')}>
<HelperHintIcon aria-label="help-tooltip" />
diff --git a/server/sonar-web/src/main/js/components/shared/SoftwareImpactPillList.tsx b/server/sonar-web/src/main/js/components/shared/SoftwareImpactPillList.tsx
index de84d2d5437..56cbea01a84 100644
--- a/server/sonar-web/src/main/js/components/shared/SoftwareImpactPillList.tsx
+++ b/server/sonar-web/src/main/js/components/shared/SoftwareImpactPillList.tsx
@@ -20,16 +20,15 @@
import classNames from 'classnames';
import React from 'react';
import { translate } from '../../helpers/l10n';
-import { SoftwareImpactSeverity, SoftwareQuality } from '../../types/clean-code-taxonomy';
+import {
+ SoftwareImpact,
+ SoftwareImpactSeverity,
+ SoftwareQuality,
+} from '../../types/clean-code-taxonomy';
import SoftwareImpactPill from './SoftwareImpactPill';
-interface SoftwareImpact {
- softwareQuality: SoftwareQuality;
- severity: SoftwareImpactSeverity;
-}
-
interface SoftwareImpactPillListProps extends React.HTMLAttributes<HTMLUListElement> {
- softwareImpacts: Array<SoftwareImpact>;
+ softwareImpacts: SoftwareImpact[];
className?: string;
type?: Parameters<typeof SoftwareImpactPill>[0]['type'];
}
diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts
index ec5a1800c61..0c0806f8403 100644
--- a/server/sonar-web/src/main/js/helpers/constants.ts
+++ b/server/sonar-web/src/main/js/helpers/constants.ts
@@ -20,6 +20,7 @@
import { colors } from '../app/theme';
import { AlmKeys } from '../types/alm-settings';
import {
+ CleanCodeAttribute,
CleanCodeAttributeCategory,
SoftwareImpactSeverity,
SoftwareQuality,
@@ -41,6 +42,31 @@ export const IMPACT_SEVERITIES = Object.values(SoftwareImpactSeverity);
export const CLEAN_CODE_CATEGORIES = Object.values(CleanCodeAttributeCategory);
+export const CLEAN_CODE_ATTRIBUTES_BY_CATEGORY = {
+ [CleanCodeAttributeCategory.Consistent]: [
+ CleanCodeAttribute.Conventional,
+ CleanCodeAttribute.Identifiable,
+ CleanCodeAttribute.Formatted,
+ ],
+ [CleanCodeAttributeCategory.Intentional]: [
+ CleanCodeAttribute.Logical,
+ CleanCodeAttribute.Clear,
+ CleanCodeAttribute.Complete,
+ CleanCodeAttribute.Efficient,
+ ],
+ [CleanCodeAttributeCategory.Adaptable]: [
+ CleanCodeAttribute.Focused,
+ CleanCodeAttribute.Distinct,
+ CleanCodeAttribute.Modular,
+ CleanCodeAttribute.Tested,
+ ],
+ [CleanCodeAttributeCategory.Responsible]: [
+ CleanCodeAttribute.Trustworthy,
+ CleanCodeAttribute.Lawful,
+ CleanCodeAttribute.Respectful,
+ ],
+};
+
export const SOFTWARE_QUALITIES = Object.values(SoftwareQuality);
export const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED'];
diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts
index e857c060594..acd56af28be 100644
--- a/server/sonar-web/src/main/js/helpers/testMocks.ts
+++ b/server/sonar-web/src/main/js/helpers/testMocks.ts
@@ -623,7 +623,7 @@ export function mockRuleActivation(overrides: Partial<RuleActivation> = {}): Rul
export function mockRuleDetails(overrides: Partial<RuleDetails> = {}): RuleDetails {
return {
- cleanCodeAttributeCategory: CleanCodeAttributeCategory.Adaptable,
+ cleanCodeAttributeCategory: CleanCodeAttributeCategory.Intentional,
cleanCodeAttribute: CleanCodeAttribute.Clear,
key: 'squid:S1337',
repo: 'squid',
diff --git a/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts b/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts
index 80e944c5e1e..28816bacad1 100644
--- a/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts
+++ b/server/sonar-web/src/main/js/types/clean-code-taxonomy.ts
@@ -53,3 +53,8 @@ export enum SoftwareQuality {
Reliability = 'RELIABILITY',
Maintainability = 'MAINTAINABILITY',
}
+
+export interface SoftwareImpact {
+ softwareQuality: SoftwareQuality;
+ severity: SoftwareImpactSeverity;
+}
diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts
index 223164569e9..a0865352dae 100644
--- a/server/sonar-web/src/main/js/types/issues.ts
+++ b/server/sonar-web/src/main/js/types/issues.ts
@@ -20,8 +20,7 @@
import {
CleanCodeAttribute,
CleanCodeAttributeCategory,
- SoftwareImpactSeverity,
- SoftwareQuality,
+ SoftwareImpact,
} from './clean-code-taxonomy';
import { Issue, Paging, TextRange } from './types';
import { UserBase } from './users';
@@ -127,10 +126,7 @@ export interface RawIssue {
author?: string;
cleanCodeAttributeCategory: CleanCodeAttributeCategory;
cleanCodeAttribute: CleanCodeAttribute;
- impacts: Array<{
- softwareQuality: SoftwareQuality;
- severity: SoftwareImpactSeverity;
- }>;
+ impacts: SoftwareImpact[];
codeVariants?: string[];
comments?: Comment[];
creationDate: string;
diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts
index 5dad670348d..9a11700a27a 100644
--- a/server/sonar-web/src/main/js/types/types.ts
+++ b/server/sonar-web/src/main/js/types/types.ts
@@ -21,8 +21,7 @@ import { RuleDescriptionSection } from '../apps/coding-rules/rule';
import {
CleanCodeAttribute,
CleanCodeAttributeCategory,
- SoftwareImpactSeverity,
- SoftwareQuality,
+ SoftwareImpact,
} from './clean-code-taxonomy';
import { ComponentQualifier, Visibility } from './component';
import { IssueStatus, IssueTransition, MessageFormatting } from './issues';
@@ -262,10 +261,7 @@ export interface Issue {
branch?: string;
cleanCodeAttributeCategory: CleanCodeAttributeCategory;
cleanCodeAttribute: CleanCodeAttribute;
- impacts: Array<{
- softwareQuality: SoftwareQuality;
- severity: SoftwareImpactSeverity;
- }>;
+ impacts: SoftwareImpact[];
codeVariants?: string[];
comments?: IssueComment[];
component: string;
@@ -544,10 +540,7 @@ export type RawQuery = Dict<any>;
export interface Rule {
cleanCodeAttributeCategory?: CleanCodeAttributeCategory;
cleanCodeAttribute?: CleanCodeAttribute;
- impacts: Array<{
- softwareQuality: SoftwareQuality;
- severity: SoftwareImpactSeverity;
- }>;
+ impacts: SoftwareImpact[];
isTemplate?: boolean;
key: string;
lang?: string;
@@ -578,7 +571,6 @@ export interface RulesUpdateRequest {
remediation_fn_base_effort?: string;
remediation_fn_type?: string;
remediation_fy_gap_multiplier?: string;
- severity?: string;
status?: string;
tags?: string;
}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index a03c68331d3..a6c112d939b 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -36,6 +36,7 @@ by_=by
calendar=Calendar
cancel=Cancel
category=Category
+attribute=Attribute
see_changelog=See Changelog
changelog=Changelog
change_verb=Change
@@ -2319,6 +2320,8 @@ coding_rules.create_custom_rule=Create Custom Rule
coding_rules.custom_rule=Custom Rule
coding_rules.custom_rule.help=Custom rules are created by administrators from templates, and are the only fully-editable rules.
coding_rules.custom_rule.activation_notice=Note: parameters of a custom rule are not customizable on rule activation, only during creation/edit.
+coding_rules.custom_rule.software_quality_x={quality} software quality
+coding_rules.custom_rule.select_software_quality=Please select at least one software quality.
coding_rules.custom_rule.removal=Only custom rules may be deleted. When a custom rule is deleted, it is not removed from the SonarQube instance. Instead its status is set to "REMOVED", allowing relevant issues to continue to be displayed properly.
coding_rules.custom_rules=Custom Rules
coding_rules.deactivate_in_quality_profile=Deactivate In Quality Profile