rule.htmlNote = data.markdown_note !== undefined ? data.markdown_note : rule.htmlNote;
rule.name = data.name !== undefined ? data.name : rule.name;
rule.status = rule.status === RuleStatus.Removed ? RuleStatus.Ready : rule.status;
+ rule.cleanCodeAttribute =
+ data.cleanCodeAttribute !== undefined ? data.cleanCodeAttribute : rule.cleanCodeAttribute;
+ rule.impacts = data.impacts !== undefined ? data.impacts : rule.impacts;
+ rule.type = data.type !== undefined ? data.type : rule.type;
+ rule.severity = data.severity !== undefined ? data.severity : rule.severity;
+
if (template && data.params) {
rule.params = [];
data.params.split(';').forEach((param) => {
RuleActivation,
RuleDetails,
RulesUpdateRequest,
+ RuleType,
} from '../types/types';
const RULES_ENDPOINT = '/api/v2/clean-code-policy/rules';
export interface CreateRuleData {
- cleanCodeAttribute: CleanCodeAttribute;
+ cleanCodeAttribute?: CleanCodeAttribute;
impacts: SoftwareImpact[];
key: string;
markdownDescription: string;
name: string;
parameters?: Partial<RestRuleParameter>[];
+ severity?: string;
status?: string;
templateKey: string;
+ type?: RuleType;
}
export function getRulesApp(): Promise<GetRulesAppResponse> {
}
export function updateRule(data: RulesUpdateRequest): Promise<RuleDetails> {
- return postJSON('/api/rules/update', data).then((r) => r.rule, throwGlobalError);
+ const impacts =
+ data.impacts &&
+ Object.values(data.impacts)
+ .map((impact) => `${impact.softwareQuality}=${impact.severity}`)
+ .join(';');
+
+ return postJSON('/api/rules/update', { ...data, impacts }).then((r) => r.rule, throwGlobalError);
}
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 { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
+import { IssueSeverity, IssueType } from '../../../types/issues';
+import { SettingsKey } from '../../../types/settings';
import { getPageObjects, renderCodingRulesApp } from '../utils-tests';
const rulesHandler = new CodingRulesServiceMock();
});
describe('custom rule', () => {
- it('can create custom rule', async () => {
+ it('can create custom rule in MQR mode', async () => {
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser());
byRole('option', { name: 'severity_impact.MEDIUM severity_impact.MEDIUM' }).get(),
);
- expect(ui.createCustomRuleDialog.byText('severity_impact.MEDIUM').get()).toBeInTheDocument();
+ expect(
+ ui.createCustomRuleDialog.byRole('combobox', { name: 'severity' }).getAll()[1],
+ ).toHaveValue('severity_impact.MEDIUM');
await user.click(ui.statusSelect.get());
await user.click(byRole('option', { name: 'rules.status.BETA' }).get());
expect(ui.customRuleItemLink('New Custom Rule').get()).toBeInTheDocument();
});
+ it('hides severities if security hotspot is selected in MQR mode', async () => {
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+ await ui.detailsloaded();
+
+ // Create custom rule
+ await user.click(ui.createCustomRuleButton.get());
+ // Switch type to Security hotspot
+ await user.click(ui.cctIssueTypeSelect.get());
+ await user.click(
+ byRole('option', { name: 'coding_rules.custom.type.option.SECURITY_HOTSPOT' }).get(),
+ );
+ expect(ui.cleanCodeCategorySelect.query()).not.toBeInTheDocument();
+
+ // Switch type back to Issue
+ await user.click(ui.cctIssueTypeSelect.get());
+ await user.click(byRole('option', { name: 'coding_rules.custom.type.option.ISSUE' }).get());
+ expect(ui.cleanCodeCategorySelect.get()).toBeInTheDocument();
+ });
+
+ it('can create custom rule in Standard mode', async () => {
+ settingsHandler.set(SettingsKey.MQRMode, 'false');
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser());
+ await ui.facetsLoaded();
+
+ await user.click(await ui.templateFacet.find());
+ 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');
+
+ // Select type as bug
+ await user.click(ui.standardIssueTypeSelect.get());
+ await user.click(byRole('option', { name: 'issue.type.BUG' }).get());
+
+ // Select Severity as Major
+ await user.click(ui.standardSeveritySelect.get());
+ await user.click(byRole('option', { name: 'severity.MAJOR' }).get());
+
+ expect(ui.createCustomRuleDialog.byRole('combobox', { name: 'severity' }).get()).toHaveValue(
+ 'severity.MAJOR',
+ );
+
+ await user.click(ui.statusSelect.get());
+ await user.click(byRole('option', { name: 'rules.status.BETA' }).get());
+
+ 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('hides severities if security hotspot is selected in Standard mode', async () => {
+ settingsHandler.set(SettingsKey.MQRMode, 'false');
+ const { ui, user } = getPageObjects();
+ rulesHandler.setIsAdmin();
+ renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+ await ui.detailsloaded();
+
+ // Create custom rule
+ await user.click(ui.createCustomRuleButton.get());
+ // Switch type to Security hotspot
+ await user.click(ui.standardIssueTypeSelect.get());
+ await user.click(byRole('option', { name: 'issue.type.SECURITY_HOTSPOT' }).get());
+
+ expect(ui.standardSeveritySelect.query()).not.toBeInTheDocument();
+
+ // Switch type back to Bug
+ await user.click(ui.standardIssueTypeSelect.get());
+ await user.click(byRole('option', { name: 'issue.type.BUG' }).get());
+ expect(ui.standardSeveritySelect.get()).toBeInTheDocument();
+ });
+
it('can reactivate custom rule', async () => {
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
expect(ui.customRuleItemLink('Reactivate custom Rule').get()).toBeInTheDocument();
});
- it('can edit custom rule', async () => {
+ it('can edit custom rule in MQR mode', 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');
+
+ // Maintainability should not be checked and should be disabled
+ expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Maintainability).get()).not.toBeChecked();
+ expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Maintainability).get()).toHaveAttribute(
+ 'aria-disabled',
+ 'true',
+ );
+ expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Reliability).get()).toHaveAttribute(
+ 'aria-disabled',
+ 'true',
+ );
+ expect(ui.cleanCodeQualityCheckbox(SoftwareQuality.Reliability).get()).toBeChecked();
+
+ // Set severity
+ await user.click(ui.cleanCodeSeveritySelect(SoftwareQuality.Reliability).get());
+ await user.click(byRole('option', { name: 'severity_impact.HIGH severity_impact.HIGH' }).get());
+
+ await user.click(ui.saveButton.get(ui.updateCustomRuleDialog.get()));
+
+ expect(ui.ruleTitle('Updated custom rule name').get()).toBeInTheDocument();
+ expect(
+ ui.ruleSoftwareQualityPill(SoftwareQuality.Reliability, SoftwareImpactSeverity.High).get(),
+ ).toBeInTheDocument();
+ });
+
+ it('can edit custom rule in Standard Mode', async () => {
+ settingsHandler.set(SettingsKey.MQRMode, 'false');
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
await user.type(ui.ruleNameTextbox.get(), 'Updated custom rule name');
await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
+ // Type should be Bug and should be disabled
+ expect(ui.standardIssueTypeSelect.get()).toHaveValue('issue.type.BUG');
+ expect(ui.standardIssueTypeSelect.get()).toBeDisabled();
+
+ // Select Severity as INFO
+ await user.click(ui.standardSeveritySelect.get());
+ await user.click(byRole('option', { name: 'severity.INFO' }).get());
+
await user.click(ui.saveButton.get(ui.updateCustomRuleDialog.get()));
expect(ui.ruleTitle('Updated custom rule name').get()).toBeInTheDocument();
+ expect(ui.ruleIssueTypePill(IssueType.Bug).get()).toBeInTheDocument();
+ expect(ui.ruleIssueTypePillSeverity(IssueSeverity.Info).get()).toBeInTheDocument();
});
it('can delete custom rule', async () => {
customRule={customRule}
onClose={() => setModalOpen(false)}
templateRule={templateRule}
+ isOpen={modalOpen}
/>
)}
</>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import { Checkbox, Select, Text } from '@sonarsource/echoes-react';
+import { useEffect, useMemo, useRef } from 'react';
import { useIntl } from 'react-intl';
-import {
- Checkbox,
- FormField,
- Highlight,
- InputSelect,
- LightPrimary,
- RequiredIcon,
- TextError,
-} from '~design-system';
+import { FormField, RequiredIcon } from '~design-system';
import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon';
import {
CLEAN_CODE_ATTRIBUTES_BY_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}
+ <Select
+ data={categories}
+ id="coding-rules-custom-clean-code-category"
+ onChange={(option) =>
+ option ? props.onChange(option as CleanCodeAttributeCategory) : undefined
+ }
isDisabled={disabled}
isSearchable={false}
- value={categories.find((category) => category.value === value)}
+ isNotClearable
+ value={categories.find((category) => category.value === value)?.value}
/>
</FormField>
);
props: Readonly<Props<CleanCodeAttribute> & { category: CleanCodeAttributeCategory }>,
) {
const { value, disabled, category, onChange } = props;
- const initialAttribute = React.useRef(value);
+ const initialAttribute = useRef(value);
const intl = useIntl();
const attributes = CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].map((attribute) => ({
}));
// Set default CC attribute when category changes
- React.useEffect(() => {
+ useEffect(() => {
if (CLEAN_CODE_ATTRIBUTES_BY_CATEGORY[category].includes(value)) {
return;
}
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}
+ <Select
+ data={attributes}
+ id="coding-rules-custom-clean-code-attribute"
+ onChange={(option) => props.onChange(option as CleanCodeAttribute)}
isDisabled={disabled}
isSearchable={false}
- value={attributes.find((attribute) => attribute.value === value)}
+ isNotClearable
+ value={attributes.find((attribute) => attribute.value === value)?.value}
/>
</FormField>
);
}
export function SoftwareQualitiesFields(
- props: Readonly<Props<SoftwareImpact[]> & { error: boolean }>,
+ props: Readonly<Props<SoftwareImpact[]> & { error: boolean; qualityUpdateDisabled: boolean }>,
) {
- const { value, disabled, error } = props;
+ const { value, disabled, error, qualityUpdateDisabled } = props;
const intl = useIntl();
- const severities = React.useMemo(
+ const severities = useMemo(
() =>
IMPACT_SEVERITIES.map((severity) => ({
value: severity,
label: intl.formatMessage({ id: `severity_impact.${severity}` }),
- Icon: <SoftwareImpactSeverityIcon severity={severity} />,
+ prefix: <SoftwareImpactSeverityIcon severity={severity} />,
})),
[intl],
);
- const handleSoftwareQualityChange = (quality: SoftwareQuality, checked: boolean) => {
- if (checked) {
+ const handleSoftwareQualityChange = (quality: SoftwareQuality, checked: boolean | string) => {
+ if (checked === true) {
props.onChange([
...value,
{ softwareQuality: quality, severity: SoftwareImpactSeverity.Low },
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">
+ <Text isHighlighted 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">
+ </Text>
+ <Text isHighlighted className="sw-w-full">
{intl.formatMessage({ id: 'severity' })}
<RequiredIcon aria-label={intl.formatMessage({ id: 'required' })} className="sw-ml-1" />
- </Highlight>
+ </Text>
</legend>
{SOFTWARE_QUALITIES.map((quality) => {
const selectedQuality = value.find((impact) => impact.softwareQuality === quality);
const selectedSeverity = selectedQuality
- ? severities.find((severity) => severity.value === selectedQuality.severity)
+ ? severities.find((severity) => severity.value === selectedQuality.severity)?.value
: null;
return (
)}
</legend>
<Checkbox
- className="sw-w-full"
+ className="sw-w-full sw-items-center"
+ isDisabled={qualityUpdateDisabled}
checked={Boolean(selectedQuality)}
onCheck={(checked) => {
handleSoftwareQualityChange(quality, checked);
}}
- label={quality}
- >
- <LightPrimary className="sw-ml-3">
- {intl.formatMessage({ id: `software_quality.${quality}` })}
- </LightPrimary>
- </Checkbox>
- <InputSelect
+ label={
+ <Text className="sw-ml-3">
+ {intl.formatMessage({ id: `software_quality.${quality}` })}
+ </Text>
+ }
+ />
+
+ <Select
+ id={`coding-rules-custom-software-impact-severity-${quality}`}
aria-label={intl.formatMessage({ id: 'severity' })}
className="sw-w-full"
- options={severities}
+ data={severities}
placeholder={intl.formatMessage({ id: 'none' })}
- onChange={(option) =>
- handleSeverityChange(quality, option?.value as SoftwareImpactSeverity)
- }
- isClearable={false}
+ onChange={(option) => handleSeverityChange(quality, option as SoftwareImpactSeverity)}
isDisabled={disabled || !selectedQuality}
isSearchable={false}
+ isNotClearable
value={selectedSeverity}
+ valueIcon={<SoftwareImpactSeverityIcon severity={selectedSeverity} />}
/>
</fieldset>
);
})}
{error && (
- <TextError
+ <Text
+ colorOverride="echoes-color-text-danger"
className="sw-font-regular sw-absolute sw--bottom-3"
- text={intl.formatMessage({ id: 'coding_rules.custom_rule.select_software_quality' })}
- />
+ >
+ {intl.formatMessage({ id: 'coding_rules.custom_rule.select_software_quality' })}
+ </Text>
)}
</fieldset>
);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Button, ButtonVariety, Modal, ModalSize, Select, Text } from '@sonarsource/echoes-react';
import { HttpStatusCode } from 'axios';
-import * as React from 'react';
+import { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
- ButtonPrimary,
FlagMessage,
FormField,
InputField,
- InputSelect,
InputTextArea,
LabelValueSelectOption,
- LightLabel,
- Modal,
SafeHTMLInjection,
SanitizeLevel,
} from '~design-system';
-import { Status } from '~sonar-aligned/types/common';
import FormattingTips from '../../../components/common/FormattingTips';
+import IssueTypeIcon from '../../../components/icon-mappers/IssueTypeIcon';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { RULE_STATUSES } from '../../../helpers/constants';
+import { RULE_STATUSES, RULE_TYPES } from '../../../helpers/constants';
import { csvEscape } from '../../../helpers/csv';
import { translate } from '../../../helpers/l10n';
import { latinize } from '../../../helpers/strings';
import { useCreateRuleMutation, useUpdateRuleMutation } from '../../../queries/rules';
+import { useStandardExperienceMode } from '../../../queries/settings';
import {
CleanCodeAttribute,
CleanCodeAttributeCategory,
SoftwareImpact,
} from '../../../types/clean-code-taxonomy';
-import { Dict, RuleDetails, RuleParameter } from '../../../types/types';
+import { CustomRuleType, Dict, RuleDetails, RuleParameter, RuleType } from '../../../types/types';
import {
CleanCodeAttributeField,
CleanCodeCategoryField,
SoftwareQualitiesFields,
} from './CustomRuleFormFieldsCCT';
+import { SeveritySelect } from './SeveritySelect';
interface Props {
customRule?: RuleDetails;
+ isOpen: boolean;
onClose: () => void;
templateRule: RuleDetails;
}
const FORM_ID = 'custom-rule-form';
export default function CustomRuleFormModal(props: Readonly<Props>) {
- const { customRule, templateRule } = props;
- const [description, setDescription] = React.useState(customRule?.mdDesc ?? '');
- const [key, setKey] = React.useState(customRule?.key ?? '');
- const [keyModifiedByUser, setKeyModifiedByUser] = React.useState(false);
- const [name, setName] = React.useState(customRule?.name ?? '');
- const [params, setParams] = React.useState(getParams(customRule));
- const [reactivating, setReactivating] = React.useState(false);
- const [status, setStatus] = React.useState(customRule?.status ?? templateRule.status);
- const [ccCategory, setCCCategory] = React.useState<CleanCodeAttributeCategory>(
+ const { customRule, templateRule, isOpen } = props;
+ const { data: isStandardMode } = useStandardExperienceMode();
+ const [description, setDescription] = useState(customRule?.mdDesc ?? '');
+ const [key, setKey] = useState(customRule?.key ?? '');
+ const [keyModifiedByUser, setKeyModifiedByUser] = useState(false);
+ const [name, setName] = useState(customRule?.name ?? '');
+ const [params, setParams] = useState(getParams(customRule));
+ const [reactivating, setReactivating] = useState(false);
+ const [status, setStatus] = useState(customRule?.status ?? templateRule.status);
+ const [ccCategory, setCCCategory] = useState<CleanCodeAttributeCategory>(
templateRule.cleanCodeAttributeCategory ?? CleanCodeAttributeCategory.Consistent,
);
- const [ccAttribute, setCCAtribute] = React.useState<CleanCodeAttribute>(
+ const [ccAttribute, setCCAttribute] = useState<CleanCodeAttribute>(
templateRule.cleanCodeAttribute ?? CleanCodeAttribute.Conventional,
);
- const [impacts, setImpacts] = React.useState<SoftwareImpact[]>(templateRule?.impacts ?? []);
+ const [impacts, setImpacts] = useState<SoftwareImpact[]>(templateRule?.impacts ?? []);
+ const [standardSeverity, setStandardSeverity] = useState(
+ customRule?.severity ?? templateRule.severity,
+ );
+ const [standardType, setStandardType] = useState(customRule?.type ?? templateRule.type);
+ const [cctType, setCCTType] = useState<CustomRuleType>(
+ standardType === 'SECURITY_HOTSPOT' ? CustomRuleType.SECURITY_HOTSPOT : CustomRuleType.ISSUE,
+ );
const customRulesSearchParams = {
f: 'name,severity,params',
template_key: templateRule.key,
setReactivating(response.status === HttpStatusCode.Conflict);
},
);
- const warningRef = React.useRef<HTMLDivElement>(null);
+ const warningRef = useRef<HTMLDivElement>(null);
const submitting = updatingRule || creatingRule;
- const hasError = impacts.length === 0;
+ const hasError =
+ !isStandardMode && impacts.length === 0 && cctType !== CustomRuleType.SECURITY_HOTSPOT;
+ const isDisabledInUpdate = submitting || customRule !== undefined;
const submit = () => {
+ const isSecurityHotspot =
+ standardType === 'SECURITY_HOTSPOT' || cctType === CustomRuleType.SECURITY_HOTSPOT;
const stringifiedParams = Object.keys(params)
.map((key) => `${key}=${csvEscape(params[key])}`)
.join(';');
+
const ruleData = {
name,
status,
markdownDescription: description,
};
+ const standardRule = {
+ type: standardType,
+ ...(isSecurityHotspot ? {} : { severity: standardSeverity }),
+ };
+
+ const cctRule = isSecurityHotspot
+ ? { type: cctType as RuleType }
+ : {
+ cleanCodeAttribute: ccAttribute,
+ impacts,
+ };
+
if (customRule) {
updateRule({
...ruleData,
+ ...(isStandardMode ? standardRule : cctRule),
params: stringifiedParams,
key: customRule.key,
});
} else if (reactivating) {
updateRule({
...ruleData,
+ ...(isStandardMode ? standardRule : cctRule),
params: stringifiedParams,
key: `${templateRule.repo}:${key}`,
});
} else {
createRule({
...ruleData,
+ impacts: [], // impacts are required in createRule
+ ...(isStandardMode ? standardRule : cctRule),
key: `${templateRule.repo}:${key}`,
templateKey: templateRule.key,
- cleanCodeAttribute: ccAttribute,
- impacts,
parameters: Object.entries(params).map(([key, value]) => ({ key, defaultValue: value })),
});
}
};
// If key changes, then most likely user did it to create a new rule instead of reactivating one
- React.useEffect(() => {
+ useEffect(() => {
setReactivating(false);
}, [key]);
// scroll to warning when it appears
- React.useEffect(() => {
+ useEffect(() => {
if (reactivating) {
warningRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [reactivating]);
- const NameField = React.useMemo(
+ const NameField = useMemo(
() => (
<FormField
ariaLabel={translate('name')}
autoFocus
disabled={submitting}
id="coding-rules-custom-rule-creation-name"
- onChange={({
- currentTarget: { value: name },
- }: React.SyntheticEvent<HTMLInputElement>) => {
+ onChange={({ currentTarget: { value: name } }: SyntheticEvent<HTMLInputElement>) => {
setName(name);
setKey(keyModifiedByUser ? key : latinize(name).replace(/[^A-Za-z0-9]/g, '_'));
}}
[key, keyModifiedByUser, name, submitting],
);
- const KeyField = React.useMemo(
+ const KeyField = useMemo(
() => (
<FormField
ariaLabel={translate('key')}
<InputField
disabled={submitting}
id="coding-rules-custom-rule-creation-key"
- onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
+ onChange={(event: SyntheticEvent<HTMLInputElement>) => {
setKey(event.currentTarget.value);
setKeyModifiedByUser(true);
}}
[customRule, key, submitting],
);
- const DescriptionField = React.useMemo(
+ const DescriptionField = useMemo(
() => (
<FormField
ariaLabel={translate('description')}
<InputTextArea
disabled={submitting}
id="coding-rules-custom-rule-creation-html-description"
- onChange={(event: React.SyntheticEvent<HTMLTextAreaElement>) =>
+ onChange={(event: SyntheticEvent<HTMLTextAreaElement>) =>
setDescription(event.currentTarget.value)
}
required
[description, submitting],
);
- const StatusField = React.useMemo(() => {
- const statusesOptions = RULE_STATUSES.map((status: Status) => ({
+ const CCTIssueTypeField = useMemo(() => {
+ const typeOptions = Object.values(CustomRuleType).map((value) => ({
+ label: translate(`coding_rules.custom.type.option.${value}`),
+ value,
+ }));
+
+ return (
+ <FormField
+ ariaLabel={translate('coding_rules.custom.type.label')}
+ label={translate('coding_rules.custom.type.label')}
+ htmlFor="coding-rules-custom-rule-type"
+ >
+ <Select
+ isRequired
+ id="coding-rules-custom-rule-type"
+ isDisabled={isDisabledInUpdate}
+ aria-labelledby="coding-rules-custom-rule-type"
+ onChange={(value) => (value ? setCCTType(value as CustomRuleType) : '')}
+ data={typeOptions}
+ isSearchable={false}
+ value={typeOptions.find((s) => s.value === cctType)?.value}
+ />
+ </FormField>
+ );
+ }, [cctType, isDisabledInUpdate]);
+
+ const StatusField = useMemo(() => {
+ const statusesOptions = RULE_STATUSES.map((status) => ({
label: translate('rules.status', status),
value: status,
}));
label={translate('coding_rules.filters.status')}
htmlFor="coding-rules-custom-rule-status"
>
- <InputSelect
- inputId="coding-rules-custom-rule-status"
- isClearable={false}
+ <Select
+ isRequired
+ id="coding-rules-custom-rule-status"
isDisabled={submitting}
aria-labelledby="coding-rules-custom-rule-status"
- onChange={({ value }: LabelValueSelectOption<Status>) => setStatus(value)}
- options={statusesOptions}
+ onChange={(value) => (value ? setStatus(value) : undefined)}
+ data={statusesOptions}
isSearchable={false}
- value={statusesOptions.find((s) => s.value === status)}
+ value={statusesOptions.find((s) => s.value === status)?.value}
/>
</FormField>
);
}, [status, submitting]);
- const handleParameterChange = React.useCallback(
- (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ const StandardTypeField = useMemo(() => {
+ const ruleTypeOption: LabelValueSelectOption<RuleType>[] = RULE_TYPES.map((type) => ({
+ label: translate('issue.type', type),
+ value: type,
+ prefix: <IssueTypeIcon type={type} />,
+ }));
+ return (
+ <FormField
+ ariaLabel={translate('type')}
+ label={translate('type')}
+ htmlFor="coding-rules-custom-rule-type"
+ >
+ <Select
+ id="coding-rules-custom-rule-type"
+ isNotClearable
+ isDisabled={isDisabledInUpdate}
+ isSearchable={false}
+ onChange={(value) => setStandardType(value as RuleType)}
+ data={ruleTypeOption}
+ value={ruleTypeOption.find((t) => t.value === standardType)?.value}
+ valueIcon={<IssueTypeIcon type={standardType} />}
+ />
+ </FormField>
+ );
+ }, [isDisabledInUpdate, standardType]);
+
+ const StandardSeverityField = useMemo(
+ () => (
+ <FormField
+ ariaLabel={translate('severity')}
+ label={translate('severity')}
+ htmlFor="coding-rules-severity-select"
+ >
+ <SeveritySelect
+ id="coding-rules-severity-select"
+ isDisabled={submitting}
+ onChange={(value) => setStandardSeverity(value)}
+ severity={standardSeverity}
+ recommendedSeverity={templateRule.severity ?? customRule?.severity}
+ />
+ </FormField>
+ ),
+ [customRule?.severity, standardSeverity, submitting, templateRule.severity],
+ );
+
+ const handleParameterChange = useCallback(
+ (event: SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = event.currentTarget;
setParams({ ...params, [name]: value });
},
[params],
);
- const renderParameterField = React.useCallback(
+ const renderParameterField = useCallback(
(param: RuleParameter) => {
// Gets the actual value from params from the state.
- // Without it, we have a issue with string 'constructor' as key
+ // Without it, we have an issue with string 'constructor' as key
const actualValue = new Map(Object.entries(params)).get(param.key) ?? '';
return (
htmlAsString={param.htmlDesc}
sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
>
- <LightLabel />
+ <Text isSubdued />
</SafeHTMLInjection>
)}
</FormField>
}
return (
<Modal
- headerTitle={header}
- onClose={props.onClose}
- body={
+ size={ModalSize.Wide}
+ isOpen={isOpen}
+ title={header}
+ onOpenChange={props.onClose}
+ content={
<form
className="sw-flex sw-flex-col sw-justify-stretch sw-pb-4"
id={FORM_ID}
- onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
+ onSubmit={(event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
submit();
}}
{NameField}
{KeyField}
- {/* do not allow to change CCT fields of existing rule */}
- {!customRule && !reactivating && (
+ {isStandardMode && (
<>
- <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}
- />
+ {StandardTypeField}
+ {standardType !== 'SECURITY_HOTSPOT' && StandardSeverityField}
+ </>
+ )}
+ {!isStandardMode && (
+ <>
+ {CCTIssueTypeField}
+ {cctType !== 'SECURITY_HOTSPOT' && (
+ <>
+ <div className="sw-flex sw-justify-between sw-gap-6">
+ <CleanCodeCategoryField
+ value={ccCategory}
+ disabled={isDisabledInUpdate}
+ onChange={setCCCategory}
+ />
+ <CleanCodeAttributeField
+ value={ccAttribute}
+ category={ccCategory}
+ disabled={isDisabledInUpdate}
+ onChange={setCCAttribute}
+ />
+ </div>
+ <SoftwareQualitiesFields
+ error={hasError}
+ value={impacts}
+ onChange={setImpacts}
+ disabled={submitting}
+ qualityUpdateDisabled={isDisabledInUpdate}
+ />
+ </>
+ )}
</>
)}
{StatusField}
</form>
}
primaryButton={
- <ButtonPrimary disabled={submitting || hasError} type="submit" form={FORM_ID}>
+ <Button
+ variety={ButtonVariety.Primary}
+ isDisabled={submitting || hasError}
+ type="submit"
+ form={FORM_ID}
+ >
{buttonText}
- </ButtonPrimary>
+ </Button>
+ }
+ secondaryButton={
+ <Button onClick={props.onClose} variety={ButtonVariety.Default}>
+ {translate('cancel')}
+ </Button>
}
- loading={submitting}
- secondaryButtonLabel={translate('cancel')}
/>
);
}
*/
import styled from '@emotion/styled';
-import { Button, ButtonVariety } from '@sonarsource/echoes-react';
import {
- ButtonSecondary,
- HelperHintIcon,
+ Button,
+ ButtonVariety,
+ Heading,
+ IconQuestionMark,
+ ModalAlert,
Spinner,
- SubHeadingHighlight,
- themeBorder,
- themeColor,
-} from '~design-system';
+} from '@sonarsource/echoes-react';
+import { useIntl } from 'react-intl';
+import { themeBorder, themeColor } from '~design-system';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
import { Profile } from '../../../api/quality-profiles';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
import DateFormatter from '../../../components/intl/DateFormatter';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
import {
useDeleteRuleMutation,
useRuleDetailsQuery,
selectedProfile,
referencedRepositories,
} = props;
+ const intl = useIntl();
const { isLoading: loadingRule, data } = useRuleDetailsQuery({
actives: true,
key: ruleKey,
const { rule: ruleDetails, actives = [] } = data ?? {};
const params = ruleDetails?.params ?? [];
- const isCustom = !!ruleDetails?.templateKey;
+ const isCustom = ruleDetails?.templateKey !== undefined;
const isEditable = canWrite && !!allowCustomRules && isCustom;
const handleTagsChange = (tags: string[]) => {
return (
<StyledRuleDetails className="it__coding-rule-details sw-p-6 sw-mt-6">
- <Spinner loading={loadingRule}>
+ <Spinner isLoading={loadingRule}>
{ruleDetails && (
<>
<RuleDetailsHeader
{/* it's expected to pass the same rule to both parameters */}
<CustomRuleButton customRule={ruleDetails} templateRule={ruleDetails}>
{({ onClick }) => (
- <ButtonSecondary
+ <Button
+ variety={ButtonVariety.Default}
className="js-edit-custom"
id="coding-rules-detail-custom-rule-change"
onClick={onClick}
>
{translate('edit')}
- </ButtonSecondary>
+ </Button>
)}
</CustomRuleButton>
- <ConfirmButton
- confirmButtonText={translate('delete')}
- isDestructive
- modalBody={translateWithParameters(
- 'coding_rules.delete.custom.confirm',
- ruleDetails.name,
+ <ModalAlert
+ title={translate('coding_rules.delete_rule')}
+ description={intl.formatMessage(
+ {
+ id: 'coding_rules.delete.custom.confirm',
+ },
+ {
+ name: ruleDetails.name,
+ },
)}
- modalHeader={translate('coding_rules.delete_rule')}
- onConfirm={() => deleteRule({ key: ruleKey })}
+ primaryButton={
+ <Button
+ className="sw-ml-2 js-delete"
+ id="coding-rules-detail-rule-delete"
+ onClick={() => deleteRule({ key: ruleKey })}
+ variety={ButtonVariety.DangerOutline}
+ >
+ {translate('delete')}
+ </Button>
+ }
+ secondaryButtonLabel={translate('close')}
>
- {({ onClick }) => (
- <>
- <Button
- className="sw-ml-2 js-delete"
- id="coding-rules-detail-rule-delete"
- onClick={onClick}
- variety={ButtonVariety.DangerOutline}
- >
- {translate('delete')}
- </Button>
- <HelpTooltip
- className="sw-ml-2"
- overlay={
- <div className="sw-py-4">
- {translate('coding_rules.custom_rule.removal')}
- </div>
- }
- >
- <HelperHintIcon />
- </HelpTooltip>
- </>
- )}
- </ConfirmButton>
+ <Button
+ className="sw-ml-2 js-delete"
+ id="coding-rules-detail-rule-delete"
+ variety={ButtonVariety.DangerOutline}
+ >
+ {translate('delete')}
+ </Button>
+ </ModalAlert>
+ <HelpTooltip
+ className="sw-ml-2"
+ overlay={
+ <div className="sw-py-4">{translate('coding_rules.custom_rule.removal')}</div>
+ }
+ >
+ <IconQuestionMark />
+ </HelpTooltip>
</div>
)}
)}
<div className="sw-my-8" data-meta="available-since">
- <SubHeadingHighlight as="h3">
- {translate('coding_rules.available_since')}
- </SubHeadingHighlight>
+ <Heading as="h3">{translate('coding_rules.available_since')}</Heading>
<DateFormatter date={ruleDetails.createdAt} />
</div>
</>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Button, ButtonVariety } from '@sonarsource/echoes-react';
-import { sortBy } from 'lodash';
-import * as React from 'react';
import {
- ButtonSecondary,
- ContentCell,
- HeadingDark,
+ Button,
+ ButtonVariety,
+ Heading,
Link,
+ ModalAlert,
Spinner,
- Table,
- TableRow,
- UnorderedList,
-} from '~design-system';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+} from '@sonarsource/echoes-react';
+import { sortBy } from 'lodash';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { ContentCell, Table, TableRow, UnorderedList } from '~design-system';
+import { translate } from '../../../helpers/l10n';
import { getRuleUrl } from '../../../helpers/urls';
import { useDeleteRuleMutation, useSearchRulesQuery } from '../../../queries/rules';
import { Rule, RuleDetails } from '../../../types/types';
return (
<div className="js-rule-custom-rules">
<div>
- <HeadingDark as="h2">{translate('coding_rules.custom_rules')}</HeadingDark>
+ <Heading as="h2">{translate('coding_rules.custom_rules')}</Heading>
{props.canChange && (
<CustomRuleButton templateRule={ruleDetails}>
{({ onClick }) => (
- <ButtonSecondary className="js-create-custom-rule sw-mt-6" onClick={onClick}>
+ <Button
+ variety={ButtonVariety.Default}
+ className="js-create-custom-rule sw-mt-6"
+ onClick={onClick}
+ >
{translate('coding_rules.create')}
- </ButtonSecondary>
+ </Button>
)}
</CustomRuleButton>
)}
))}
</Table>
)}
- <Spinner className="sw-my-6" loading={loading} />
+ <Spinner className="sw-my-6" isLoading={loading} />
</div>
</div>
);
}>,
) {
const { rule, editable } = props;
+ const intl = useIntl();
return (
<TableRow data-rule={rule.key}>
<ContentCell>
{editable && (
<ContentCell>
- <ConfirmButton
- confirmButtonText={translate('delete')}
- confirmData={rule.key}
- isDestructive
- modalBody={translateWithParameters('coding_rules.delete.custom.confirm', rule.name)}
- modalHeader={translate('coding_rules.delete_rule')}
- onConfirm={props.onDelete}
- >
- {({ onClick }) => (
+ <ModalAlert
+ title={translate('coding_rules.delete_rule')}
+ description={intl.formatMessage(
+ {
+ id: 'coding_rules.delete.custom.confirm',
+ },
+ {
+ name: rule.name,
+ },
+ )}
+ primaryButton={
<Button
- className="js-delete-custom-rule"
- aria-label={translateWithParameters('coding_rules.delete_rule_x', rule.name)}
- onClick={onClick}
+ className="sw-ml-2 js-delete"
+ id="coding-rules-detail-rule-delete"
+ onClick={() => props.onDelete(rule.key)}
variety={ButtonVariety.DangerOutline}
>
{translate('delete')}
</Button>
- )}
- </ConfirmButton>
+ }
+ secondaryButtonLabel={translate('close')}
+ >
+ <Button
+ className="js-delete-custom-rule"
+ aria-label={intl.formatMessage(
+ { id: 'coding_rules.delete_rule_x' },
+ { name: rule.name },
+ )}
+ variety={ButtonVariety.DangerOutline}
+ >
+ {translate('delete')}
+ </Button>
+ </ModalAlert>
</ContentCell>
)}
</TableRow>
import { HelperText, Select } from '@sonarsource/echoes-react';
import { isEmpty } from 'lodash';
import { FormattedMessage, useIntl } from 'react-intl';
-import SeverityIcon from '../../../components/icon-mappers/SeverityIcon';
import SoftwareImpactSeverityIcon from '../../../components/icon-mappers/SoftwareImpactSeverityIcon';
import { SEVERITIES } from '../../../helpers/constants';
import { SoftwareImpactSeverity } from '../../../types/clean-code-taxonomy';
export function SeveritySelect(props: SeveritySelectProps) {
const { isDisabled, severity, recommendedSeverity, impactSeverity, id } = props;
const intl = useIntl();
- const Icon = impactSeverity ? SoftwareImpactSeverityIcon : SeverityIcon;
const getSeverityTranslation = (severity: string) =>
impactSeverity
? intl.formatMessage({ id: `severity_impact.${severity}` })
: intl.formatMessage({ id: `severity.${severity}` });
- const serverityOption = (impactSeverity ? Object.values(SoftwareImpactSeverity) : SEVERITIES).map(
+ const severityOption = (impactSeverity ? Object.values(SoftwareImpactSeverity) : SEVERITIES).map(
(severity) => ({
label:
severity === recommendedSeverity
)
: getSeverityTranslation(severity),
value: severity,
- prefix: <Icon severity={severity} aria-hidden />,
+ prefix: <SoftwareImpactSeverityIcon severity={severity} aria-hidden />,
}),
);
id={id}
isDisabled={isDisabled}
onChange={props.onChange}
- data={serverityOption}
+ data={severityOption}
isSearchable={false}
isNotClearable
placeholder={
isDisabled && !isEmpty(severity) ? intl.formatMessage({ id: 'not_impacted' }) : undefined
}
value={severity}
- valueIcon={<Icon severity={severity} aria-hidden />}
+ valueIcon={<SoftwareImpactSeverityIcon severity={severity} aria-hidden />}
/>
{severity !== recommendedSeverity && (
<HelperText className="sw-mt-2">
import {
CleanCodeAttribute,
CleanCodeAttributeCategory,
+ SoftwareImpactSeverity,
SoftwareQuality,
} from '../../types/clean-code-taxonomy';
import { Feature } from '../../types/features';
+import { IssueSeverity, IssueType } from '../../types/issues';
import { CurrentUser } from '../../types/users';
import routes from './routes';
ruleCleanCodeAttribute: (attribute: CleanCodeAttribute) =>
byText(new RegExp(`rule\\.clean_code_attribute\\.${attribute}$`)),
ruleSoftwareQuality: (quality: SoftwareQuality) => byText(`software_quality.${quality}`),
+ ruleSoftwareQualityPill: (quality: SoftwareQuality, severity: SoftwareImpactSeverity) =>
+ byRole('button', { name: `software_quality.${quality} severity_impact.${severity}` }),
+ ruleIssueTypePill: (issueType: IssueType) => byRole('banner').byText(`issue.type.${issueType}`),
+ ruleIssueTypePillSeverity: (severity: IssueSeverity) =>
+ byRole('banner').byLabelText(`severity.${severity}`),
// Rule tags
tagsDropdown: byLabelText(/tags_list_x/).byRole('button'),
deleteCustomRuleDialog: byRole('alertdialog', { name: 'coding_rules.delete_rule' }),
ruleNameTextbox: byRole('textbox', { name: 'name' }),
keyTextbox: byRole('textbox', { name: 'key' }),
+ cctIssueTypeSelect: byRole('combobox', { name: 'coding_rules.custom.type.label' }),
+ standardIssueTypeSelect: byRole('combobox', { name: 'type' }),
cleanCodeCategorySelect: byRole('combobox', { name: 'category' }),
cleanCodeAttributeSelect: byRole('combobox', { name: 'attribute' }),
cleanCodeQualityCheckbox: (quality: SoftwareQuality) =>
'combobox',
{ name: 'severity' },
),
+ standardSeveritySelect: byRole('combobox', { name: 'severity' }),
statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
descriptionTextbox: byRole('textbox', { name: 'description' }),
createButton: byRole('button', { name: 'create' }),
}
export interface RulesUpdateRequest {
+ cleanCodeAttribute?: CleanCodeAttribute;
+ impacts?: SoftwareImpact[];
key: string;
markdownDescription?: string;
markdown_note?: string;
remediation_fn_base_effort?: string;
remediation_fn_type?: string;
remediation_fy_gap_multiplier?: string;
+ severity?: string;
status?: string;
tags?: string;
+ type?: RuleType;
}
export interface RuleDetails extends Rule {
] as const;
export type RuleType = (typeof RuleTypes)[number];
+export enum CustomRuleType {
+ ISSUE = 'ISSUE',
+ SECURITY_HOTSPOT = 'SECURITY_HOTSPOT',
+}
+
export interface Snippet {
end: number;
index: number;
coding_rules.deactivate_in_quality_profile=Deactivate In Quality Profile
coding_rules.deactivate_in_quality_profile_x=Deactivate In Quality Profile {0}
coding_rules.delete_rule=Delete Rule
-coding_rules.delete_rule_x=Delete Rule {0}
-coding_rules.delete.custom.confirm=Are you sure you want to delete custom rule "{0}"?
+coding_rules.delete_rule_x=Delete Rule {name}
+coding_rules.delete.custom.confirm=Are you sure you want to delete custom rule "{name}"?
coding_rules.extend_description=Extend Description
coding_rules.deactivate_in=Deactivate In
coding_rules.deactivate=Deactivate
coding_rules.select_profile=Select Profile
coding_rules.selected_profiles=Selected Profiles
+coding_rules.custom.type.label=Type
+coding_rules.custom.type.option.ISSUE=Issue
+coding_rules.custom.type.option.SECURITY_HOTSPOT=Security Hotspot
+
coding_rules.system_tags_tooltip=This tag can't be removed because it has been predefined by our system
rule.impact.severity.tooltip=Issues found for this rule will have a {severity} impact on the {quality} of your software.