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;
+ rule.tags = data.tags !== undefined ? data.tags.split(',') : rule.tags;
return this.reply(rule);
};
return postJSON('/api/qualityprofiles/deactivate_rules', data);
}
-export function activateRule(data: {
+export interface ActivateRuleParameters {
key: string;
params?: Dict<string>;
reset?: boolean;
rule: string;
severity?: string;
-}) {
+}
+
+export function activateRule(data: ActivateRuleParameters) {
const params =
data.params && map(data.params, (value, key) => `${key}=${csvEscape(value)}`).join(';');
return post('/api/qualityprofiles/activate_rule', { ...data, params }).catch(throwGlobalError);
}
-export function deactivateRule(data: { key: string; rule: string }) {
+export interface DeactivateRuleParameters {
+ key: string;
+ rule: string;
+}
+
+export function deactivateRule(data: DeactivateRuleParameters) {
return post('/api/qualityprofiles/deactivate_rule', data).catch(throwGlobalError);
}
it('shows rule with default description section and params', async () => {
const { ui } = getPageObjects();
renderCodingRulesApp(undefined, 'coding_rules?open=rule1');
- await ui.appLoaded();
+ await ui.detailsloaded();
expect(ui.ruleTitle('Awsome java rule').get()).toBeInTheDocument();
expect(
ui.ruleCleanCodeAttributeCategory(CleanCodeAttributeCategory.Adaptable).get(),
it('shows external rule', async () => {
const { ui } = getPageObjects();
renderCodingRulesApp(undefined, 'coding_rules?open=rule6');
- await ui.appLoaded();
+ await ui.detailsloaded();
expect(ui.ruleTitle('Bad Python rule').get()).toBeInTheDocument();
expect(ui.externalDescription('Bad Python rule').get()).toBeInTheDocument();
});
it('shows hotspot rule', async () => {
const { ui, user } = getPageObjects();
renderCodingRulesApp(undefined, 'coding_rules?open=rule2');
- await ui.appLoaded();
+ await ui.detailsloaded();
expect(ui.ruleTitle('Hot hotspot').get()).toBeInTheDocument();
expect(ui.introTitle.get()).toBeInTheDocument();
it('shows rule advanced section', async () => {
const { ui } = getPageObjects();
renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
- await ui.appLoaded();
+ await ui.detailsloaded();
expect(ui.ruleTitle('Awsome Python rule').get()).toBeInTheDocument();
expect(ui.introTitle.get()).toBeInTheDocument();
// Shows correct tabs
it('shows rule advanced section with context', async () => {
const { ui, user } = getPageObjects();
renderCodingRulesApp(undefined, 'coding_rules?open=rule7');
- await ui.appLoaded();
+ await ui.detailsloaded();
expect(ui.ruleTitle('Python rule with context').get()).toBeInTheDocument();
await user.click(ui.howToFixTab.get());
it('should show CYAC notification for rule advanced section and removes it after user`s visit', async () => {
const { ui, user } = getPageObjects();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule10');
- await ui.appLoaded();
+ await ui.detailsloaded();
await user.click(ui.moreInfoTab.get());
expect(ui.caycNotificationButton.get()).toBeInTheDocument();
it('should show CAYC notification for rule advanced section and removes it when user scrolls to the principles', async () => {
const { ui, user } = getPageObjects();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule10');
- await ui.appLoaded();
+ await ui.detailsloaded();
await user.click(ui.moreInfoTab.get());
expect(ui.caycNotificationButton.get()).toBeInTheDocument();
const { ui, user } = getPageObjects();
renderCodingRulesApp(mockCurrentUser(), 'coding_rules?open=rule10');
- await ui.appLoaded();
+ await ui.detailsloaded();
await user.click(ui.moreInfoTab.get());
expect(ui.caycNotificationButton.query()).not.toBeInTheDocument();
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
- await ui.appLoaded();
+ await ui.detailsloaded();
expect(ui.qpLink('QP Foo').get()).toBeInTheDocument();
// Activate rule in quality profile
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
- await ui.appLoaded();
+ await ui.detailsloaded();
// Should show 2 deactivate buttons: one for the parent, one for the child profile.
expect(ui.deactivateInQPButton('QP FooBarBaz').get()).toBeInTheDocument();
rulesHandler.setIsAdmin();
settingsHandler.set(SettingsKey.QPAdminCanDisableInheritedRules, 'false');
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule1');
- await ui.appLoaded();
+ await ui.detailsloaded();
// Should show 1 deactivate button: one for the parent, none for the child profile.
expect(ui.deactivateInQPButton('QP FooBarBaz').get()).toBeInTheDocument();
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(undefined, 'coding_rules?open=rule5');
- await ui.appLoaded();
+ await ui.detailsloaded();
expect(ui.ruleTitle('Awsome Python rule').get()).toBeInTheDocument();
// Add
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(undefined, 'coding_rules?open=rule10');
- await ui.appLoaded();
+ await ui.detailsloaded();
await user.click(ui.tagsDropdown.get());
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
- await ui.appLoaded();
+ await ui.detailsloaded();
await user.click(ui.editCustomRuleButton.get());
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule9');
- await ui.appLoaded();
+ await ui.detailsloaded();
await user.click(ui.deleteButton.get());
await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
const { ui, user } = getPageObjects();
rulesHandler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
- await ui.appLoaded();
+ await ui.detailsloaded();
await user.click(ui.deleteCustomRuleButton('Custom Rule based on rule8').get());
await user.click(ui.deleteButton.get(ui.deleteCustomRuleDialog.get()));
buttonText: string;
className?: string;
modalHeader: string;
- onDone: (severity: string) => Promise<void>;
+ onDone?: (severity: string) => Promise<void> | void;
profiles: BaseProfile[];
rule: Rule | RuleDetails;
ariaLabel?: string;
Note,
} from 'design-system';
import * as React from 'react';
-import { Profile, activateRule } from '../../../api/quality-profiles';
+import { Profile } from '../../../api/quality-profiles';
import DocLink from '../../../components/common/DocLink';
import { translate } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
+import { useActivateRuleMutation } from '../../../queries/quality-profiles';
import { IssueSeverity } from '../../../types/issues';
import { Dict, Rule, RuleActivation, RuleDetails } from '../../../types/types';
import { sortProfiles } from '../../quality-profiles/utils';
activation?: RuleActivation;
modalHeader: string;
onClose: () => void;
- onDone: (severity: string) => Promise<void>;
- // eslint-disable-next-line react/no-unused-prop-types
+ onDone?: (severity: string) => Promise<void> | void;
profiles: Profile[];
rule: Rule | RuleDetails;
}
depth: number;
}
-interface State {
- params: Dict<string>;
- profile?: ProfileWithDepth;
- submitting: boolean;
- severity: IssueSeverity;
-}
-
const MIN_PROFILES_TO_ENABLE_SELECT = 2;
const FORM_ID = 'rule-activation-modal-form';
-export default class ActivationFormModal extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- const profilesWithDepth = this.getQualityProfilesWithDepth(props);
- this.state = {
- params: this.getParams(props),
- profile: profilesWithDepth.length > 0 ? profilesWithDepth[0] : undefined,
- submitting: false,
- severity: (props.activation
- ? props.activation.severity
- : props.rule.severity) as IssueSeverity,
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- getParams = ({ activation, rule } = this.props) => {
- const params: Dict<string> = {};
- if (rule?.params) {
- for (const param of rule.params) {
- params[param.key] = param.defaultValue || '';
- }
- if (activation?.params) {
- for (const param of activation.params) {
- params[param.key] = param.value;
- }
- }
- }
- return params;
- };
-
- // Choose QP which a user can administrate, which are the same language and which are not built-in
- getQualityProfilesWithDepth = ({ profiles } = this.props) => {
- return sortProfiles(
- profiles.filter(
- (profile) =>
- !profile.isBuiltIn &&
- profile.actions &&
- profile.actions.edit &&
- profile.language === this.props.rule.lang,
- ),
- ).map((profile) => ({
- ...profile,
- // Decrease depth by 1, so the top level starts at 0
- depth: profile.depth - 1,
- }));
- };
-
- handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+export default function ActivationFormModal(props: Readonly<Props>) {
+ const { activation, rule, profiles, modalHeader } = props;
+ const { mutate: activateRule, isLoading: submitting } = useActivateRuleMutation((data) => {
+ props.onDone?.(data.severity as string);
+ props.onClose();
+ });
+
+ const profilesWithDepth = getQualityProfilesWithDepth(profiles, rule.lang);
+ const [profile, setProfile] = React.useState(profilesWithDepth[0]);
+ const [params, setParams] = React.useState(getRuleParams({ activation, rule }));
+ const [severity, setSeverity] = React.useState(
+ (activation ? activation.severity : rule.severity) as IssueSeverity,
+ );
+
+ const profileOptions = profilesWithDepth.map((p) => ({ label: p.name, value: p }));
+ const isCustomRule = !!(rule as RuleDetails).templateKey;
+ const activeInAllProfiles = profilesWithDepth.length <= 0;
+ const isUpdateMode = !!activation;
+
+ const handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
- this.setState({ submitting: true });
const data = {
- key: this.state.profile?.key ?? '',
- params: this.state.params,
- rule: this.props.rule.key,
- severity: this.state.severity,
+ key: profile?.key ?? '',
+ params,
+ rule: rule.key,
+ severity,
};
- activateRule(data)
- .then(() => this.props.onDone(data.severity))
- .then(
- () => {
- if (this.mounted) {
- this.setState({ submitting: false });
- this.props.onClose();
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ submitting: false });
- }
- },
- );
+ activateRule(data);
};
- handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ const handleParameterChange = (
+ event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>,
+ ) => {
const { name, value } = event.currentTarget;
- this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
- };
-
- handleProfileChange = (value: LabelValueSelectOption<ProfileWithDepth>) => {
- this.setState({ profile: value.value });
- };
-
- handleSeverityChange = ({ value }: LabelValueSelectOption<IssueSeverity>) => {
- this.setState({ severity: value });
+ setParams({ ...params, [name]: value });
};
- render() {
- const { activation, rule } = this.props;
- const { profile, severity, submitting } = this.state;
- const { params = [] } = rule;
- const profilesWithDepth = this.getQualityProfilesWithDepth();
- const profileOptions = profilesWithDepth.map((p) => ({ label: p.name, value: p }));
- const isCustomRule = !!(rule as RuleDetails).templateKey;
- const activeInAllProfiles = profilesWithDepth.length <= 0;
- const isUpdateMode = !!activation;
-
- return (
- <Modal
- headerTitle={this.props.modalHeader}
- onClose={this.props.onClose}
- loading={submitting}
- isOverflowVisible
- primaryButton={
- <ButtonPrimary disabled={submitting || activeInAllProfiles} form={FORM_ID} type="submit">
- {isUpdateMode ? translate('save') : translate('coding_rules.activate')}
- </ButtonPrimary>
- }
- secondaryButtonLabel={translate('cancel')}
- body={
- <form id={FORM_ID} onSubmit={this.handleFormSubmit}>
- {!isUpdateMode && activeInAllProfiles && (
- <FlagMessage className="sw-mb-2" variant="info">
- {translate('coding_rules.active_in_all_profiles')}
- </FlagMessage>
- )}
-
- <FlagMessage className="sw-mb-4" variant="info">
- {translate('coding_rules.severity_deprecated')}
- <DocLink className="sw-ml-2 sw-whitespace-nowrap" to="/user-guide/clean-code/">
- {translate('learn_more')}
- </DocLink>
+ return (
+ <Modal
+ headerTitle={modalHeader}
+ onClose={props.onClose}
+ loading={submitting}
+ isOverflowVisible
+ primaryButton={
+ <ButtonPrimary disabled={submitting || activeInAllProfiles} form={FORM_ID} type="submit">
+ {isUpdateMode ? translate('save') : translate('coding_rules.activate')}
+ </ButtonPrimary>
+ }
+ secondaryButtonLabel={translate('cancel')}
+ body={
+ <form id={FORM_ID} onSubmit={handleFormSubmit}>
+ {!isUpdateMode && activeInAllProfiles && (
+ <FlagMessage className="sw-mb-2" variant="info">
+ {translate('coding_rules.active_in_all_profiles')}
</FlagMessage>
+ )}
+
+ <FlagMessage className="sw-mb-4" variant="info">
+ {translate('coding_rules.severity_deprecated')}
+ <DocLink className="sw-ml-2 sw-whitespace-nowrap" to="/user-guide/clean-code/">
+ {translate('learn_more')}
+ </DocLink>
+ </FlagMessage>
+
+ <FormField
+ ariaLabel={translate('coding_rules.quality_profile')}
+ label={translate('coding_rules.quality_profile')}
+ htmlFor="coding-rules-quality-profile-select-input"
+ >
+ <InputSelect
+ id="coding-rules-quality-profile-select"
+ inputId="coding-rules-quality-profile-select-input"
+ isClearable={false}
+ isDisabled={submitting || profilesWithDepth.length < MIN_PROFILES_TO_ENABLE_SELECT}
+ onChange={({ value }: LabelValueSelectOption<ProfileWithDepth>) => {
+ setProfile(value);
+ }}
+ getOptionLabel={({ value }: LabelValueSelectOption<ProfileWithDepth>) =>
+ ' '.repeat(value.depth) + value.name
+ }
+ options={profileOptions}
+ value={profileOptions.find(({ value }) => value.key === profile?.key)}
+ />
+ </FormField>
+
+ <FormField
+ ariaLabel={translate('severity')}
+ label={translate('severity')}
+ htmlFor="coding-rules-severity-select"
+ >
+ <SeveritySelect
+ isDisabled={submitting}
+ onChange={({ value }: LabelValueSelectOption<IssueSeverity>) => {
+ setSeverity(value);
+ }}
+ severity={severity}
+ />
+ </FormField>
+
+ {isCustomRule ? (
+ <Note as="p" className="sw-my-4">
+ {translate('coding_rules.custom_rule.activation_notice')}
+ </Note>
+ ) : (
+ rule.params?.map((param) => (
+ <FormField label={param.key} key={param.key} htmlFor={param.key}>
+ {param.type === 'TEXT' ? (
+ <InputTextArea
+ id={param.key}
+ disabled={submitting}
+ name={param.key}
+ onChange={handleParameterChange}
+ placeholder={param.defaultValue}
+ rows={3}
+ size="full"
+ value={params[param.key] ?? ''}
+ />
+ ) : (
+ <InputField
+ id={param.key}
+ disabled={submitting}
+ name={param.key}
+ onChange={handleParameterChange}
+ placeholder={param.defaultValue}
+ size="full"
+ type="text"
+ value={params[param.key] ?? ''}
+ />
+ )}
+ {param.htmlDesc !== undefined && (
+ <Note
+ as="div"
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
+ />
+ )}
+ </FormField>
+ ))
+ )}
+ </form>
+ }
+ />
+ );
+}
- <FormField
- ariaLabel={translate('coding_rules.quality_profile')}
- label={translate('coding_rules.quality_profile')}
- htmlFor="coding-rules-quality-profile-select-input"
- >
- <InputSelect
- id="coding-rules-quality-profile-select"
- inputId="coding-rules-quality-profile-select-input"
- isClearable={false}
- isDisabled={submitting || profilesWithDepth.length < MIN_PROFILES_TO_ENABLE_SELECT}
- onChange={this.handleProfileChange}
- getOptionLabel={({ value }: LabelValueSelectOption<ProfileWithDepth>) =>
- ' '.repeat(value.depth) + value.name
- }
- options={profileOptions}
- value={profileOptions.find(({ value }) => value.key === profile?.key)}
- />
- </FormField>
-
- <FormField
- ariaLabel={translate('severity')}
- label={translate('severity')}
- htmlFor="coding-rules-severity-select"
- >
- <SeveritySelect
- isDisabled={submitting}
- onChange={this.handleSeverityChange}
- severity={severity}
- />
- </FormField>
+function getQualityProfilesWithDepth(
+ profiles: Profile[] = [],
+ ruleLang?: string,
+): ProfileWithDepth[] {
+ return sortProfiles(
+ profiles.filter(
+ (profile) =>
+ !profile.isBuiltIn &&
+ profile.actions &&
+ profile.actions.edit &&
+ profile.language === ruleLang,
+ ),
+ ).map((profile) => ({
+ ...profile,
+ // Decrease depth by 1, so the top level starts at 0
+ depth: profile.depth - 1,
+ }));
+}
- {isCustomRule ? (
- <Note as="p" className="sw-my-4">
- {translate('coding_rules.custom_rule.activation_notice')}
- </Note>
- ) : (
- params.map((param) => (
- <FormField label={param.key} key={param.key} htmlFor={param.key}>
- {param.type === 'TEXT' ? (
- <InputTextArea
- id={param.key}
- disabled={submitting}
- name={param.key}
- onChange={this.handleParameterChange}
- placeholder={param.defaultValue}
- rows={3}
- size="full"
- value={this.state.params[param.key] ?? ''}
- />
- ) : (
- <InputField
- id={param.key}
- disabled={submitting}
- name={param.key}
- onChange={this.handleParameterChange}
- placeholder={param.defaultValue}
- size="full"
- type="text"
- value={this.state.params[param.key] ?? ''}
- />
- )}
- {param.htmlDesc !== undefined && (
- <Note
- as="div"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
- />
- )}
- </FormField>
- ))
- )}
- </form>
- }
- />
- );
+function getRuleParams({
+ activation,
+ rule,
+}: {
+ activation?: RuleActivation;
+ rule: RuleDetails | Rule;
+}) {
+ const params: Dict<string> = {};
+ if (rule?.params) {
+ for (const param of rule.params) {
+ params[param.key] = param.defaultValue ?? '';
+ }
+ if (activation?.params) {
+ for (const param of activation.params) {
+ params[param.key] = param.value;
+ }
+ }
}
+ return params;
}
interface Props {
children: (props: { onClick: () => void }) => React.ReactNode;
customRule?: RuleDetails;
- onDone: (newRuleDetails: RuleDetails) => void;
templateRule: RuleDetails;
}
const { customRule, templateRule } = props;
const [modalOpen, setModalOpen] = React.useState(false);
- const handleDone = (newRuleDetails: RuleDetails) => {
- setModalOpen(false);
- props.onDone(newRuleDetails);
- };
-
return (
<>
{props.children({ onClick: () => setModalOpen(true) })}
<CustomRuleFormModal
customRule={customRule}
onClose={() => setModalOpen(false)}
- onDone={handleDone}
templateRule={templateRule}
/>
)}
} from 'design-system';
import * as React from 'react';
import { OptionProps, SingleValueProps, components } from 'react-select';
-import { createRule, updateRule } from '../../../api/rules';
import FormattingTips from '../../../components/common/FormattingTips';
import TypeHelper from '../../../components/shared/TypeHelper';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
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';
interface Props {
customRule?: RuleDetails;
onClose: () => void;
- onDone: (newRuleDetails: RuleDetails) => void;
templateRule: RuleDetails;
}
-interface State {
- description: string;
- key: string;
- keyModifiedByUser: boolean;
- name: string;
- params: Dict<string>;
- reactivating: boolean;
- severity: string;
- status: string;
- submitting: boolean;
- type: RuleType;
-}
-
const FORM_ID = 'custom-rule-form';
-export default class CustomRuleFormModal extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- const params: Dict<string> = {};
- if (props.customRule?.params) {
- for (const param of props.customRule.params) {
- params[param.key] = param.defaultValue ?? '';
- }
- }
- this.state = {
- description: props.customRule?.mdDesc ?? '',
- key: '',
- keyModifiedByUser: false,
- name: props.customRule?.name ?? '',
- params,
- reactivating: false,
- severity: props.customRule?.severity ?? props.templateRule.severity,
- status: props.customRule?.status ?? props.templateRule.status,
- submitting: false,
- type: props.customRule?.type ?? props.templateRule.type,
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- }
+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 [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 { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(props.onClose);
+ const { mutate: createRule, isLoading: creatingRule } = useCreateRuleMutation(
+ {
+ f: 'name,severity,params',
+ template_key: templateRule.key,
+ },
+ props.onClose,
+ (response: Response) => {
+ setReactivating(response.status === HttpStatusCode.Conflict);
+ },
+ );
- componentWillUnmount() {
- this.mounted = false;
- }
+ const submitting = updatingRule || creatingRule;
- prepareRequest = () => {
- const { customRule, templateRule } = this.props;
- const params = Object.keys(this.state.params)
- .map((key) => `${key}=${csvEscape(this.state.params[key])}`)
+ const submit = () => {
+ const stringifiedParams = Object.keys(params)
+ .map((key) => `${key}=${csvEscape(params[key])}`)
.join(';');
const ruleData = {
- markdownDescription: this.state.description,
- name: this.state.name,
- params,
- severity: this.state.severity,
- status: this.state.status,
+ markdownDescription: description,
+ name,
+ params: stringifiedParams,
+ severity,
+ status,
};
return customRule
? updateRule({ ...ruleData, key: customRule.key })
: createRule({
...ruleData,
- customKey: this.state.key,
- preventReactivation: !this.state.reactivating,
+ customKey: key,
+ preventReactivation: !reactivating,
templateKey: templateRule.key,
- type: this.state.type,
+ type,
});
};
- handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
- event.preventDefault();
- this.setState({ submitting: true });
- this.prepareRequest().then(
- (newRuleDetails) => {
- if (this.mounted) {
- this.setState({ submitting: false });
- this.props.onDone(newRuleDetails);
- }
- },
- (response: Response) => {
- if (this.mounted) {
- this.setState({
- reactivating: response.status === HttpStatusCode.Conflict,
- submitting: false,
- });
- }
- },
- );
- };
-
- handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
- const { value: name } = event.currentTarget;
- this.setState((state) => ({
- name,
- key: state.keyModifiedByUser ? state.key : latinize(name).replace(/[^A-Za-z0-9]/g, '_'),
- }));
- };
-
- handleKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ key: event.currentTarget.value, keyModifiedByUser: true });
-
- handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) =>
- this.setState({ description: event.currentTarget.value });
-
- handleTypeChange = ({ value }: LabelValueSelectOption<RuleType>) =>
- this.setState({ type: value });
-
- handleSeverityChange = ({ value }: { value: string }) => this.setState({ severity: value });
-
- handleStatusChange = ({ value }: LabelValueSelectOption<Status>) =>
- this.setState({ status: value });
-
- handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
- const { name, value } = event.currentTarget;
- this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
- };
-
- renderNameField = () => (
- <FormField
- ariaLabel={translate('name')}
- label={translate('name')}
- htmlFor="coding-rules-custom-rule-creation-name"
- required
- >
- <InputField
- autoFocus
- disabled={this.state.submitting}
- id="coding-rules-custom-rule-creation-name"
- onChange={this.handleNameChange}
+ const NameField = React.useMemo(
+ () => (
+ <FormField
+ ariaLabel={translate('name')}
+ label={translate('name')}
+ htmlFor="coding-rules-custom-rule-creation-name"
required
- size="full"
- type="text"
- value={this.state.name}
- />
- </FormField>
- );
-
- renderKeyField = () => (
- <FormField
- ariaLabel={translate('key')}
- label={translate('key')}
- htmlFor="coding-rules-custom-rule-creation-key"
- required
- >
- {this.props.customRule ? (
- <span title={this.props.customRule.key}>{this.props.customRule.key}</span>
- ) : (
+ >
<InputField
- disabled={this.state.submitting}
- id="coding-rules-custom-rule-creation-key"
- onChange={this.handleKeyChange}
+ autoFocus
+ disabled={submitting}
+ id="coding-rules-custom-rule-creation-name"
+ onChange={({
+ currentTarget: { value: name },
+ }: React.SyntheticEvent<HTMLInputElement>) => {
+ setName(name);
+ setKey(keyModifiedByUser ? key : latinize(name).replace(/[^A-Za-z0-9]/g, '_'));
+ }}
required
size="full"
type="text"
- value={this.state.key}
+ value={name}
/>
- )}
- </FormField>
+ </FormField>
+ ),
+ [key, keyModifiedByUser, name, submitting],
);
- renderDescriptionField = () => (
- <FormField
- ariaLabel={translate('description')}
- label={translate('description')}
- htmlFor="coding-rules-custom-rule-creation-html-description"
- required
- >
- <InputTextArea
- disabled={this.state.submitting}
- id="coding-rules-custom-rule-creation-html-description"
- onChange={this.handleDescriptionChange}
+ const KeyField = React.useMemo(
+ () => (
+ <FormField
+ ariaLabel={translate('key')}
+ label={translate('key')}
+ htmlFor="coding-rules-custom-rule-creation-key"
required
- rows={5}
- size="full"
- value={this.state.description}
- />
- <FormattingTips />
- </FormField>
+ >
+ {customRule ? (
+ <span title={customRule.key}>{customRule.key}</span>
+ ) : (
+ <InputField
+ disabled={submitting}
+ id="coding-rules-custom-rule-creation-key"
+ onChange={(event: React.SyntheticEvent<HTMLInputElement>) => {
+ setKey(event.currentTarget.value);
+ setKeyModifiedByUser(true);
+ }}
+ required
+ size="full"
+ type="text"
+ value={key}
+ />
+ )}
+ </FormField>
+ ),
+ [customRule, key, submitting],
);
- renderTypeOption = (props: OptionProps<LabelValueSelectOption<RuleType>, false>) => {
- return (
- <components.Option {...props}>
- <TypeHelper type={props.data.value} />
- </components.Option>
- );
- };
-
- renderTypeSingleValue = (props: SingleValueProps<LabelValueSelectOption<RuleType>, false>) => {
- return (
- <components.SingleValue {...props}>
- <TypeHelper className="display-flex-center" type={props.data.value} />
- </components.SingleValue>
- );
- };
+ const DescriptionField = React.useMemo(
+ () => (
+ <FormField
+ ariaLabel={translate('description')}
+ label={translate('description')}
+ htmlFor="coding-rules-custom-rule-creation-html-description"
+ required
+ >
+ <InputTextArea
+ disabled={submitting}
+ id="coding-rules-custom-rule-creation-html-description"
+ onChange={(event: React.SyntheticEvent<HTMLTextAreaElement>) =>
+ setDescription(event.currentTarget.value)
+ }
+ required
+ rows={5}
+ size="full"
+ value={description}
+ />
+ <FormattingTips />
+ </FormField>
+ ),
+ [description, submitting],
+ );
- renderTypeField = () => {
+ const TypeField = React.useMemo(() => {
const ruleTypeOption: LabelValueSelectOption<RuleType>[] = RULE_TYPES.map((type) => ({
label: translate('issue.type', type),
value: type,
<InputSelect
inputId="coding-rules-custom-rule-type"
isClearable={false}
- isDisabled={this.state.submitting}
+ isDisabled={submitting}
isSearchable={false}
- onChange={this.handleTypeChange}
+ onChange={({ value }: LabelValueSelectOption<RuleType>) => setType(value)}
components={{
- Option: this.renderTypeOption,
- SingleValue: this.renderTypeSingleValue,
+ Option: TypeSelectOption,
+ SingleValue: TypeSelectValue,
}}
options={ruleTypeOption}
- value={ruleTypeOption.find((t) => t.value === this.state.type)}
+ value={ruleTypeOption.find((t) => t.value === type)}
/>
</FormField>
);
- };
+ }, [type, submitting]);
- renderSeverityField = () => (
- <FormField
- ariaLabel={translate('severity')}
- label={translate('severity')}
- htmlFor="coding-rules-severity-select"
- >
- <SeveritySelect
- isDisabled={this.state.submitting}
- onChange={this.handleSeverityChange}
- severity={this.state.severity}
- />
- </FormField>
+ 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],
);
- renderStatusField = () => {
+ const StatusField = React.useMemo(() => {
const statusesOptions = RULE_STATUSES.map((status) => ({
label: translate('rules.status', status),
value: status,
}));
+
return (
<FormField
ariaLabel={translate('coding_rules.filters.status')}
<InputSelect
inputId="coding-rules-custom-rule-status"
isClearable={false}
- isDisabled={this.state.submitting}
+ isDisabled={submitting}
aria-labelledby="coding-rules-custom-rule-status"
- onChange={this.handleStatusChange}
+ onChange={({ value }: LabelValueSelectOption<Status>) => setStatus(value)}
options={statusesOptions}
isSearchable={false}
- value={statusesOptions.find((s) => s.value === this.state.status)}
+ value={statusesOptions.find((s) => s.value === status)}
/>
</FormField>
);
- };
+ }, [status, submitting]);
- renderParameterField = (param: RuleParameter) => {
- // Gets the actual value from params from the state.
- // Without it, we have a issue with string 'constructor' as key
- const actualValue = new Map(Object.entries(this.state.params)).get(param.key) ?? '';
+ const handleParameterChange = React.useCallback(
+ (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ const { name, value } = event.currentTarget;
+ setParams({ ...params, [name]: value });
+ },
+ [params],
+ );
- return (
- <FormField
- ariaLabel={param.key}
- className="sw-capitalize"
- label={param.key}
- htmlFor={`coding-rule-custom-rule-${param.key}`}
- key={param.key}
- >
- {param.type === 'TEXT' ? (
- <InputTextArea
- disabled={this.state.submitting}
- id={`coding-rule-custom-rule-${param.key}`}
- name={param.key}
- onChange={this.handleParameterChange}
- placeholder={param.defaultValue}
- size="full"
- rows={3}
- value={actualValue}
- />
- ) : (
- <InputField
- disabled={this.state.submitting}
- id={`coding-rule-custom-rule-${param.key}`}
- name={param.key}
- onChange={this.handleParameterChange}
- placeholder={param.defaultValue}
- size="full"
- type="text"
- value={actualValue}
- />
- )}
- {param.htmlDesc !== undefined && (
- <LightLabel
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
- />
- )}
- </FormField>
- );
- };
+ const renderParameterField = React.useCallback(
+ (param: RuleParameter) => {
+ // Gets the actual value from params from the state.
+ // Without it, we have a issue with string 'constructor' as key
+ const actualValue = new Map(Object.entries(params)).get(param.key) ?? '';
- render() {
- const { customRule, templateRule } = this.props;
- const { reactivating, submitting } = this.state;
- const { params = [] } = templateRule;
- const header = customRule
- ? translate('coding_rules.update_custom_rule')
- : translate('coding_rules.create_custom_rule');
- let submit = translate(customRule ? 'save' : 'create');
- if (this.state.reactivating) {
- submit = translate('coding_rules.reactivate');
- }
- return (
- <Modal
- headerTitle={header}
- onClose={this.props.onClose}
- body={
- <form
- className="sw-flex sw-flex-col sw-justify-stretch sw-pb-4"
- id={FORM_ID}
- onSubmit={this.handleFormSubmit}
- >
- {reactivating && (
- <FlagMessage variant="warning" className="sw-mb-6">
- {translate('coding_rules.reactivate.help')}
- </FlagMessage>
- )}
+ return (
+ <FormField
+ ariaLabel={param.key}
+ className="sw-capitalize"
+ label={param.key}
+ htmlFor={`coding-rule-custom-rule-${param.key}`}
+ key={param.key}
+ >
+ {param.type === 'TEXT' ? (
+ <InputTextArea
+ disabled={submitting}
+ id={`coding-rule-custom-rule-${param.key}`}
+ name={param.key}
+ onChange={handleParameterChange}
+ placeholder={param.defaultValue}
+ size="full"
+ rows={3}
+ value={actualValue}
+ />
+ ) : (
+ <InputField
+ disabled={submitting}
+ id={`coding-rule-custom-rule-${param.key}`}
+ name={param.key}
+ onChange={handleParameterChange}
+ placeholder={param.defaultValue}
+ size="full"
+ type="text"
+ value={actualValue}
+ />
+ )}
+ {param.htmlDesc !== undefined && (
+ <LightLabel
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
+ />
+ )}
+ </FormField>
+ );
+ },
+ [params, submitting, handleParameterChange],
+ );
- <MandatoryFieldsExplanation className="sw-mb-4" />
+ const { params: templateParams = [] } = templateRule;
+ const header = customRule
+ ? translate('coding_rules.update_custom_rule')
+ : translate('coding_rules.create_custom_rule');
+ let buttonText = translate(customRule ? 'save' : 'create');
+ if (reactivating) {
+ buttonText = translate('coding_rules.reactivate');
+ }
+ return (
+ <Modal
+ headerTitle={header}
+ onClose={props.onClose}
+ body={
+ <form
+ className="sw-flex sw-flex-col sw-justify-stretch sw-pb-4"
+ id={FORM_ID}
+ onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ submit();
+ }}
+ >
+ {reactivating && (
+ <FlagMessage variant="warning" className="sw-mb-6">
+ {translate('coding_rules.reactivate.help')}
+ </FlagMessage>
+ )}
- {this.renderNameField()}
- {this.renderKeyField()}
- {/* do not allow to change the type of existing rule */}
- {!customRule && this.renderTypeField()}
- {this.renderSeverityField()}
- {this.renderStatusField()}
- {this.renderDescriptionField()}
- {params.map(this.renderParameterField)}
- </form>
- }
- primaryButton={
- <ButtonPrimary disabled={submitting} type="submit" form={FORM_ID}>
- {submit}
- </ButtonPrimary>
- }
- loading={submitting}
- secondaryButtonLabel={translate('cancel')}
- />
- );
+ <MandatoryFieldsExplanation className="sw-mb-4" />
+
+ {NameField}
+ {KeyField}
+ {/* do not allow to change the type of existing rule */}
+ {!customRule && TypeField}
+ {SeverityField}
+ {StatusField}
+ {DescriptionField}
+ {templateParams.map(renderParameterField)}
+ </form>
+ }
+ primaryButton={
+ <ButtonPrimary disabled={submitting} type="submit" form={FORM_ID}>
+ {buttonText}
+ </ButtonPrimary>
+ }
+ loading={submitting}
+ secondaryButtonLabel={translate('cancel')}
+ />
+ );
+}
+
+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> = {};
+
+ if (customRule?.params) {
+ for (const param of customRule.params) {
+ params[param.key] = param.defaultValue ?? '';
+ }
}
+
+ return params;
}
} from 'design-system';
import * as React from 'react';
import { Profile } from '../../../api/quality-profiles';
-import { deleteRule, getRuleDetails, updateRule } from '../../../api/rules';
import ConfirmButton from '../../../components/controls/ConfirmButton';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import DateFormatter from '../../../components/intl/DateFormatter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { Dict, RuleActivation, RuleDetails as TypeRuleDetails } from '../../../types/types';
+import {
+ useDeleteRuleMutation,
+ useRuleDetailsQuery,
+ useUpdateRuleMutation,
+} from '../../../queries/rules';
+import { Dict } from '../../../types/types';
import { Activation } from '../query';
import CustomRuleButton from './CustomRuleButton';
import RuleDetailsCustomRules from './RuleDetailsCustomRules';
selectedProfile?: Profile;
}
-interface State {
- actives?: RuleActivation[];
- loading: boolean;
- ruleDetails?: TypeRuleDetails;
-}
-
-export default class RuleDetails extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: true };
-
- componentDidMount() {
- this.mounted = true;
- this.setState({ loading: true });
- this.fetchRuleDetails();
- }
-
- componentDidUpdate(prevProps: Props) {
- if (prevProps.ruleKey !== this.props.ruleKey) {
- this.setState({ loading: true });
- this.fetchRuleDetails();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchRuleDetails = () => {
- return getRuleDetails({
- actives: true,
- key: this.props.ruleKey,
- }).then(
- ({ actives, rule }) => {
- if (this.mounted) {
- this.setState({ actives, loading: false, ruleDetails: rule });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- },
- );
- };
-
- handleRuleChange = (ruleDetails: TypeRuleDetails) => {
- if (this.mounted) {
- this.setState({ ruleDetails });
- }
- };
-
- handleTagsChange = (tags: string[]) => {
- // optimistic update
- const oldTags = this.state.ruleDetails && this.state.ruleDetails.tags;
- this.setState((state) =>
- state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags } } : null,
- );
- updateRule({
- key: this.props.ruleKey,
- tags: tags.join(),
- }).catch(() => {
- if (this.mounted) {
- this.setState((state) =>
- state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags: oldTags } } : null,
- );
- }
- });
+export default function RuleDetails(props: Readonly<Props>) {
+ const {
+ ruleKey,
+ allowCustomRules,
+ canWrite,
+ referencedProfiles,
+ canDeactivateInherited,
+ selectedProfile,
+ referencedRepositories,
+ } = props;
+ const { isLoading: loadingRule, data } = useRuleDetailsQuery({
+ actives: true,
+ key: ruleKey,
+ });
+ const { mutate: updateRule } = useUpdateRuleMutation();
+ const { mutate: deleteRule } = useDeleteRuleMutation({}, props.onDelete);
+
+ const { rule: ruleDetails, actives = [] } = data ?? {};
+
+ const params = ruleDetails?.params ?? [];
+ const isCustom = !!ruleDetails?.templateKey;
+ const isEditable = canWrite && !!allowCustomRules && isCustom;
+
+ const handleTagsChange = (tags: string[]) => {
+ updateRule({ key: ruleKey, tags: tags.join() });
};
- handleActivate = () => {
- return this.fetchRuleDetails().then(() => {
- const { ruleKey, selectedProfile } = this.props;
- if (selectedProfile && this.state.actives) {
- const active = this.state.actives.find((active) => active.qProfile === selectedProfile.key);
- if (active) {
- this.props.onActivate(selectedProfile.key, ruleKey, active);
- }
+ const handleActivate = () => {
+ if (selectedProfile) {
+ const active = actives.find((active) => active.qProfile === selectedProfile.key);
+ if (active) {
+ props.onActivate(selectedProfile.key, ruleKey, active);
}
- });
- };
-
- handleDeactivate = () => {
- return this.fetchRuleDetails().then(() => {
- const { ruleKey, selectedProfile } = this.props;
- if (
- selectedProfile &&
- this.state.actives &&
- !this.state.actives.find((active) => active.qProfile === selectedProfile.key)
- ) {
- this.props.onDeactivate(selectedProfile.key, ruleKey);
- }
- });
- };
-
- handleDelete = () => {
- return deleteRule({ key: this.props.ruleKey }).then(() =>
- this.props.onDelete(this.props.ruleKey),
- );
+ }
};
- render() {
- const { ruleDetails } = this.state;
-
- if (!ruleDetails) {
- return <div className="coding-rule-details" />;
+ const handleDeactivate = () => {
+ if (selectedProfile && actives.find((active) => active.qProfile === selectedProfile.key)) {
+ props.onDeactivate(selectedProfile.key, ruleKey);
}
+ };
- const { allowCustomRules, canWrite, referencedProfiles, canDeactivateInherited } = this.props;
- const { params = [] } = ruleDetails;
-
- const isCustom = !!ruleDetails.templateKey;
- const isEditable = canWrite && !!this.props.allowCustomRules && isCustom;
-
- return (
- <StyledRuleDetails className="it__coding-rule-details sw-p-6 sw-mt-6">
- <Spinner loading={this.state.loading}>
- <RuleDetailsMeta
- canWrite={canWrite}
- onTagsChange={this.handleTagsChange}
- referencedRepositories={this.props.referencedRepositories}
- ruleDetails={ruleDetails}
- />
+ return (
+ <StyledRuleDetails className="it__coding-rule-details sw-p-6 sw-mt-6">
+ <Spinner loading={loadingRule}>
+ {ruleDetails && (
+ <>
+ <RuleDetailsMeta
+ canWrite={canWrite}
+ onTagsChange={handleTagsChange}
+ referencedRepositories={referencedRepositories}
+ ruleDetails={ruleDetails}
+ />
- <RuleDetailsDescription
- canWrite={canWrite}
- onChange={this.handleRuleChange}
- ruleDetails={ruleDetails}
- />
+ <RuleDetailsDescription canWrite={canWrite} ruleDetails={ruleDetails} />
- {params.length > 0 && <RuleDetailsParameters params={params} />}
+ {params.length > 0 && <RuleDetailsParameters params={params} />}
- {isEditable && (
- <div className="coding-rules-detail-description display-flex-center">
- {/* `templateRule` is used to get rule meta data, `customRule` is used to get parameter values */}
- {/* it's expected to pass the same rule to both parameters */}
- <CustomRuleButton
- customRule={ruleDetails}
- onDone={this.handleRuleChange}
- templateRule={ruleDetails}
- >
- {({ onClick }) => (
- <ButtonSecondary
- className="js-edit-custom"
- id="coding-rules-detail-custom-rule-change"
- onClick={onClick}
- >
- {translate('edit')}
- </ButtonSecondary>
- )}
- </CustomRuleButton>
- <ConfirmButton
- confirmButtonText={translate('delete')}
- isDestructive
- modalBody={translateWithParameters(
- 'coding_rules.delete.custom.confirm',
- ruleDetails.name,
- )}
- modalHeader={translate('coding_rules.delete_rule')}
- onConfirm={this.handleDelete}
- >
- {({ onClick }) => (
- <>
- <DangerButtonSecondary
- className="sw-ml-2 js-delete"
- id="coding-rules-detail-rule-delete"
+ {isEditable && (
+ <div className="coding-rules-detail-description display-flex-center">
+ {/* `templateRule` is used to get rule meta data, `customRule` is used to get parameter values */}
+ {/* it's expected to pass the same rule to both parameters */}
+ <CustomRuleButton customRule={ruleDetails} templateRule={ruleDetails}>
+ {({ onClick }) => (
+ <ButtonSecondary
+ className="js-edit-custom"
+ id="coding-rules-detail-custom-rule-change"
onClick={onClick}
>
- {translate('delete')}
- </DangerButtonSecondary>
- <HelpTooltip
- className="sw-ml-2"
- overlay={
- <div className="sw-py-4">
- {translate('coding_rules.custom_rule.removal')}
- </div>
- }
- >
- <HelperHintIcon />
- </HelpTooltip>
- </>
- )}
- </ConfirmButton>
+ {translate('edit')}
+ </ButtonSecondary>
+ )}
+ </CustomRuleButton>
+ <ConfirmButton
+ confirmButtonText={translate('delete')}
+ isDestructive
+ modalBody={translateWithParameters(
+ 'coding_rules.delete.custom.confirm',
+ ruleDetails.name,
+ )}
+ modalHeader={translate('coding_rules.delete_rule')}
+ onConfirm={() => deleteRule({ key: ruleKey })}
+ >
+ {({ onClick }) => (
+ <>
+ <DangerButtonSecondary
+ className="sw-ml-2 js-delete"
+ id="coding-rules-detail-rule-delete"
+ onClick={onClick}
+ >
+ {translate('delete')}
+ </DangerButtonSecondary>
+ <HelpTooltip
+ className="sw-ml-2"
+ overlay={
+ <div className="sw-py-4">
+ {translate('coding_rules.custom_rule.removal')}
+ </div>
+ }
+ >
+ <HelperHintIcon />
+ </HelpTooltip>
+ </>
+ )}
+ </ConfirmButton>
+ </div>
+ )}
+
+ {ruleDetails.isTemplate && (
+ <RuleDetailsCustomRules
+ canChange={allowCustomRules && canWrite}
+ ruleDetails={ruleDetails}
+ />
+ )}
+
+ {!ruleDetails.isTemplate && (
+ <RuleDetailsProfiles
+ activations={actives}
+ canDeactivateInherited={canDeactivateInherited}
+ onActivate={handleActivate}
+ onDeactivate={handleDeactivate}
+ referencedProfiles={referencedProfiles}
+ ruleDetails={ruleDetails}
+ />
+ )}
+
+ {!ruleDetails.isTemplate && ruleDetails.type !== 'SECURITY_HOTSPOT' && (
+ <RuleDetailsIssues ruleDetails={ruleDetails} />
+ )}
+
+ <div className="sw-my-8" data-meta="available-since">
+ <SubHeadingHighlight as="h3">
+ {translate('coding_rules.available_since')}
+ </SubHeadingHighlight>
+ <DateFormatter date={ruleDetails.createdAt} />
</div>
- )}
-
- {ruleDetails.isTemplate && (
- <RuleDetailsCustomRules
- canChange={allowCustomRules && canWrite}
- ruleDetails={ruleDetails}
- />
- )}
-
- {!ruleDetails.isTemplate && (
- <RuleDetailsProfiles
- activations={this.state.actives}
- canDeactivateInherited={canDeactivateInherited}
- onActivate={this.handleActivate}
- onDeactivate={this.handleDeactivate}
- referencedProfiles={referencedProfiles}
- ruleDetails={ruleDetails}
- />
- )}
-
- {!ruleDetails.isTemplate && ruleDetails.type !== 'SECURITY_HOTSPOT' && (
- <RuleDetailsIssues ruleDetails={ruleDetails} />
- )}
-
- <div className="sw-my-8" data-meta="available-since">
- <SubHeadingHighlight as="h3">
- {translate('coding_rules.available_since')}
- </SubHeadingHighlight>
- <DateFormatter date={ruleDetails.createdAt} />
- </div>
- </Spinner>
- </StyledRuleDetails>
- );
- }
+ </>
+ )}
+ </Spinner>
+ </StyledRuleDetails>
+ );
}
const StyledRuleDetails = styled.div`
} from 'design-system';
import { sortBy } from 'lodash';
import * as React from 'react';
-import { deleteRule, searchRules } from '../../../api/rules';
import ConfirmButton from '../../../components/controls/ConfirmButton';
import IssueSeverityIcon from '../../../components/icon-mappers/IssueSeverityIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getRuleUrl } from '../../../helpers/urls';
+import { useDeleteRuleMutation, useSearchRulesQuery } from '../../../queries/rules';
import { IssueSeverity } from '../../../types/issues';
import { Rule, RuleDetails } from '../../../types/types';
import CustomRuleButton from './CustomRuleButton';
ruleDetails: RuleDetails;
}
-interface State {
- loading: boolean;
- rules?: Rule[];
-}
-
const COLUMN_COUNT = 3;
const COLUMN_COUNT_WITH_EDIT_PERMISSIONS = 4;
-export default class RuleDetailsCustomRules extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: false };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchRules();
- }
+export default function RuleDetailsCustomRules(props: Readonly<Props>) {
+ const { ruleDetails } = props;
+ const rulesSearchParams = {
+ f: 'name,severity,params',
+ template_key: ruleDetails.key,
+ };
+ const { isLoading: loadingRules, data } = useSearchRulesQuery(rulesSearchParams);
+ const { mutate: deleteRules, isLoading: deletingRule } = useDeleteRuleMutation(rulesSearchParams);
- componentDidUpdate(prevProps: Props) {
- if (prevProps.ruleDetails.key !== this.props.ruleDetails.key) {
- this.fetchRules();
- }
- }
+ const loading = loadingRules || deletingRule;
+ const rules = data?.rules ?? [];
- componentWillUnmount() {
- this.mounted = false;
- }
+ const handleRuleDelete = React.useCallback(
+ (ruleKey: string) => {
+ deleteRules({ key: ruleKey });
+ },
+ [deleteRules],
+ );
- fetchRules = () => {
- this.setState({ loading: true });
- searchRules({
- f: 'name,severity,params',
- template_key: this.props.ruleDetails.key,
- }).then(
- ({ rules }) => {
- if (this.mounted) {
- this.setState({ rules, loading: false });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- },
- );
- };
+ return (
+ <div className="js-rule-custom-rules">
+ <div>
+ <HeadingDark as="h2">{translate('coding_rules.custom_rules')}</HeadingDark>
- handleRuleCreate = (newRuleDetails: RuleDetails) => {
- if (this.mounted) {
- this.setState(({ rules = [] }: State) => ({
- rules: [...rules, newRuleDetails],
- }));
- }
- };
-
- handleRuleDelete = (ruleKey: string) => {
- return deleteRule({ key: ruleKey }).then(() => {
- if (this.mounted) {
- this.setState(({ rules = [] }) => ({
- rules: rules.filter((rule) => rule.key !== ruleKey),
- }));
- }
- });
- };
+ {props.canChange && (
+ <CustomRuleButton templateRule={ruleDetails}>
+ {({ onClick }) => (
+ <ButtonSecondary className="js-create-custom-rule sw-mt-6" onClick={onClick}>
+ {translate('coding_rules.create')}
+ </ButtonSecondary>
+ )}
+ </CustomRuleButton>
+ )}
+ {rules.length > 0 && (
+ <Table
+ className="sw-my-6"
+ id="coding-rules-detail-custom-rules"
+ columnCount={props.canChange ? COLUMN_COUNT_WITH_EDIT_PERMISSIONS : COLUMN_COUNT}
+ >
+ {sortBy(rules, (rule) => rule.name).map((rule) => (
+ <RuleListItem
+ key={rule.key}
+ rule={rule}
+ editable={props.canChange}
+ onDelete={handleRuleDelete}
+ />
+ ))}
+ </Table>
+ )}
+ <Spinner className="sw-my-6" loading={loading} />
+ </div>
+ </div>
+ );
+}
- renderRule = (rule: Rule) => (
- <TableRow data-rule={rule.key} key={rule.key}>
+function RuleListItem(
+ props: Readonly<{
+ rule: Rule;
+ editable?: boolean;
+ onDelete: (ruleKey: string) => void;
+ }>,
+) {
+ const { rule, editable } = props;
+ return (
+ <TableRow data-rule={rule.key}>
<ContentCell>
<div>
<Link to={getRuleUrl(rule.key)}>{rule.name}</Link>
</UnorderedList>
</ContentCell>
- {this.props.canChange && (
+ {editable && (
<ContentCell>
<ConfirmButton
confirmButtonText={translate('delete')}
isDestructive
modalBody={translateWithParameters('coding_rules.delete.custom.confirm', rule.name)}
modalHeader={translate('coding_rules.delete_rule')}
- onConfirm={this.handleRuleDelete}
+ onConfirm={props.onDelete}
>
{({ onClick }) => (
<DangerButtonSecondary
)}
</TableRow>
);
-
- render() {
- const { loading, rules = [] } = this.state;
-
- return (
- <div className="js-rule-custom-rules">
- <div>
- <HeadingDark as="h2">{translate('coding_rules.custom_rules')}</HeadingDark>
-
- {this.props.canChange && (
- <CustomRuleButton onDone={this.handleRuleCreate} templateRule={this.props.ruleDetails}>
- {({ onClick }) => (
- <ButtonSecondary className="js-create-custom-rule sw-mt-6" onClick={onClick}>
- {translate('coding_rules.create')}
- </ButtonSecondary>
- )}
- </CustomRuleButton>
- )}
-
- <Spinner className="sw-my-6" loading={loading}>
- {rules.length > 0 && (
- <Table
- className="sw-my-6"
- id="coding-rules-detail-custom-rules"
- columnCount={
- this.props.canChange ? COLUMN_COUNT_WITH_EDIT_PERMISSIONS : COLUMN_COUNT
- }
- >
- {sortBy(rules, (rule) => rule.name).map(this.renderRule)}
- </Table>
- )}
- </Spinner>
- </div>
- </div>
- );
- }
}
Spinner,
} from 'design-system';
import * as React from 'react';
-import { updateRule } from '../../../api/rules';
import FormattingTips from '../../../components/common/FormattingTips';
import RuleTabViewer from '../../../components/rules/RuleTabViewer';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { sanitizeString, sanitizeUserInput } from '../../../helpers/sanitize';
+import { useUpdateRuleMutation } from '../../../queries/rules';
import { RuleDetails } from '../../../types/types';
import { RuleDescriptionSections } from '../rule';
import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
interface Props {
canWrite: boolean | undefined;
- onChange: (newRuleDetails: RuleDetails) => void;
ruleDetails: RuleDetails;
}
-interface State {
- description: string;
- descriptionForm: boolean;
- removeDescriptionModal: boolean;
- submitting: boolean;
-}
-
-export default class RuleDetailsDescription extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- description: '',
- descriptionForm: false,
- removeDescriptionModal: false,
- submitting: false,
- };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) =>
- this.setState({ description: event.currentTarget.value });
-
- handleCancelClick = () => {
- this.setState({ descriptionForm: false });
- };
-
- handleSaveClick = (event: React.SyntheticEvent<HTMLFormElement>) => {
- event.preventDefault();
- this.updateDescription(this.state.description);
- };
-
- handleRemoveDescriptionClick = () => {
- this.setState({ removeDescriptionModal: true });
- };
+export default function RuleDetailsDescription(props: Readonly<Props>) {
+ const { ruleDetails, canWrite } = props;
+ const [description, setDescription] = React.useState('');
+ const [descriptionForm, setDescriptionForm] = React.useState(false);
+ const [removeDescriptionModal, setDescriptionModal] = React.useState(false);
- handleCancelRemoving = () => this.setState({ removeDescriptionModal: false });
-
- handleConfirmRemoving = () => {
- this.setState({ removeDescriptionModal: false });
- this.updateDescription('');
- };
-
- updateDescription = (text: string) => {
- this.setState({ submitting: true });
+ const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(() =>
+ setDescriptionForm(false),
+ );
+ const updateDescription = (text = '') => {
updateRule({
- key: this.props.ruleDetails.key,
+ key: ruleDetails.key,
markdown_note: text,
- }).then(
- (ruleDetails) => {
- this.props.onChange(ruleDetails);
-
- if (this.mounted) {
- this.setState({ submitting: false, descriptionForm: false });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ submitting: false });
- }
- },
- );
- };
-
- handleExtendDescriptionClick = () => {
- this.setState({
- // set description` to the current `mdNote` each time the form is open
- description: this.props.ruleDetails.mdNote ?? '',
- descriptionForm: true,
});
};
- renderExtendedDescription = () => (
+ const renderExtendedDescription = () => (
<div id="coding-rules-detail-description-extra">
- {this.props.ruleDetails.htmlNote !== undefined && (
+ {ruleDetails.htmlNote !== undefined && (
<CodeSyntaxHighlighter
className="markdown sw-my-6"
- htmlAsString={sanitizeUserInput(this.props.ruleDetails.htmlNote)}
- language={this.props.ruleDetails.lang}
+ htmlAsString={sanitizeUserInput(ruleDetails.htmlNote)}
+ language={ruleDetails.lang}
/>
)}
<div className="sw-my-6">
- {this.props.canWrite && (
- <ButtonSecondary onClick={this.handleExtendDescriptionClick}>
+ {canWrite && (
+ <ButtonSecondary
+ onClick={() => {
+ setDescription(ruleDetails.mdNote ?? '');
+ setDescriptionForm(true);
+ }}
+ >
{translate('coding_rules.extend_description')}
</ButtonSecondary>
)}
</div>
);
- renderForm = () => (
+ const renderForm = () => (
<form
aria-label={translate('coding_rules.detail.extend_description.form')}
className="sw-my-6"
- onSubmit={this.handleSaveClick}
+ onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ updateDescription(description);
+ }}
>
<InputTextArea
aria-label={translate('coding_rules.extend_description')}
className="sw-mb-2 sw-resize-y"
id="coding-rules-detail-extend-description-text"
size="full"
- onChange={this.handleDescriptionChange}
+ onChange={({ currentTarget: { value } }: React.SyntheticEvent<HTMLTextAreaElement>) =>
+ setDescription(value)
+ }
rows={4}
- value={this.state.description}
+ value={description}
/>
<div className="sw-flex sw-items-center sw-justify-between">
<div className="sw-flex sw-items-center">
<ButtonPrimary
id="coding-rules-detail-extend-description-submit"
- disabled={this.state.submitting}
+ disabled={updatingRule}
type="submit"
>
{translate('save')}
</ButtonPrimary>
- {this.props.ruleDetails.mdNote !== undefined && (
+ {ruleDetails.mdNote !== undefined && (
<>
<DangerButtonSecondary
className="sw-ml-2"
- disabled={this.state.submitting}
+ disabled={updatingRule}
id="coding-rules-detail-extend-description-remove"
- onClick={this.handleRemoveDescriptionClick}
+ onClick={() => setDescriptionModal(true)}
>
{translate('remove')}
</DangerButtonSecondary>
- {this.state.removeDescriptionModal && (
+ {removeDescriptionModal && (
<RemoveExtendedDescriptionModal
- onCancel={this.handleCancelRemoving}
- onSubmit={this.handleConfirmRemoving}
+ onCancel={() => setDescriptionModal(false)}
+ onSubmit={() => {
+ setDescriptionModal(false);
+ updateDescription();
+ }}
/>
)}
</>
<ButtonSecondary
className="sw-ml-2"
- disabled={this.state.submitting}
+ disabled={updatingRule}
id="coding-rules-detail-extend-description-cancel"
- onClick={this.handleCancelClick}
+ onClick={() => setDescriptionForm(false)}
>
{translate('cancel')}
</ButtonSecondary>
- <Spinner className="sw-ml-2" loading={this.state.submitting} />
+ <Spinner className="sw-ml-2" loading={updatingRule} />
</div>
<FormattingTips />
</form>
);
- render() {
- const { ruleDetails } = this.props;
- const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
-
- const hasDescriptionSection =
- hasDescription &&
- ruleDetails.descriptionSections &&
- ruleDetails.descriptionSections.length > 0;
-
- const defaultSection =
- hasDescriptionSection &&
- ruleDetails.descriptionSections?.length === 1 &&
- ruleDetails.descriptionSections[0].key === RuleDescriptionSections.DEFAULT
- ? ruleDetails.descriptionSections[0]
- : undefined;
-
- const introductionSection = ruleDetails.descriptionSections?.find(
- (section) => section.key === RuleDescriptionSections.INTRODUCTION,
- )?.content;
-
- return (
- <div className="js-rule-description">
- {hasDescriptionSection && !defaultSection && (
- <>
- {introductionSection && (
- <CodeSyntaxHighlighter
- className="rule-desc"
- htmlAsString={sanitizeString(introductionSection)}
- language={ruleDetails.lang}
- />
- )}
- </>
- )}
+ const hasDescription = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
+
+ const hasDescriptionSection =
+ hasDescription && ruleDetails.descriptionSections && ruleDetails.descriptionSections.length > 0;
+
+ const defaultSection =
+ hasDescriptionSection &&
+ ruleDetails.descriptionSections?.length === 1 &&
+ ruleDetails.descriptionSections[0].key === RuleDescriptionSections.DEFAULT
+ ? ruleDetails.descriptionSections[0]
+ : undefined;
+
+ const introductionSection = ruleDetails.descriptionSections?.find(
+ (section) => section.key === RuleDescriptionSections.INTRODUCTION,
+ )?.content;
+
+ return (
+ <div className="js-rule-description">
+ {hasDescriptionSection && !defaultSection && (
+ <>
+ {introductionSection && (
+ <CodeSyntaxHighlighter
+ className="rule-desc"
+ htmlAsString={sanitizeString(introductionSection)}
+ language={ruleDetails.lang}
+ />
+ )}
+ </>
+ )}
- <RuleTabViewer ruleDetails={ruleDetails} />
+ <RuleTabViewer ruleDetails={ruleDetails} />
- {ruleDetails.isExternal && (
- <div className="coding-rules-detail-description rule-desc markdown">
- {translateWithParameters('issue.external_issue_description', ruleDetails.name)}
- </div>
- )}
+ {ruleDetails.isExternal && (
+ <div className="coding-rules-detail-description rule-desc markdown">
+ {translateWithParameters('issue.external_issue_description', ruleDetails.name)}
+ </div>
+ )}
- {!ruleDetails.templateKey && (
- <div className="sw-mt-6">
- {!this.state.descriptionForm && this.renderExtendedDescription()}
- {this.state.descriptionForm && this.props.canWrite && this.renderForm()}
- </div>
- )}
- </div>
- );
- }
+ {!ruleDetails.templateKey && (
+ <div className="sw-mt-6">
+ {!descriptionForm && renderExtendedDescription()}
+ {descriptionForm && canWrite && renderForm()}
+ </div>
+ )}
+ </div>
+ );
}
import { filter } from 'lodash';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { Profile, activateRule, deactivateRule } from '../../../api/quality-profiles';
+import { Profile } from '../../../api/quality-profiles';
import ConfirmButton from '../../../components/controls/ConfirmButton';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getQualityProfileUrl } from '../../../helpers/urls';
+import {
+ useActivateRuleMutation,
+ useDeactivateRuleMutation,
+} from '../../../queries/quality-profiles';
import { Dict, RuleActivation, RuleDetails } from '../../../types/types';
import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge';
import ActivationButton from './ActivationButton';
interface Props {
activations: RuleActivation[] | undefined;
canDeactivateInherited?: boolean;
- onActivate: () => Promise<void>;
- onDeactivate: () => Promise<void>;
+ onActivate: () => void;
+ onDeactivate: () => void;
referencedProfiles: Dict<Profile>;
ruleDetails: RuleDetails;
}
const PROFILES_HEADING_ID = 'rule-details-profiles-heading';
-export default class RuleDetailsProfiles extends React.PureComponent<Props> {
- handleActivate = () => this.props.onActivate();
+export default function RuleDetailsProfiles(props: Readonly<Props>) {
+ const { activations = [], referencedProfiles, ruleDetails, canDeactivateInherited } = props;
+ const { mutate: activateRule } = useActivateRuleMutation(props.onActivate);
+ const { mutate: deactivateRule } = useDeactivateRuleMutation(props.onDeactivate);
+
+ const canActivate = Object.values(referencedProfiles).some((profile) =>
+ Boolean(profile.actions?.edit && profile.language === ruleDetails.lang),
+ );
- handleDeactivate = (key?: string) => {
+ const handleDeactivate = (key?: string) => {
if (key !== undefined) {
deactivateRule({
key,
- rule: this.props.ruleDetails.key,
- }).then(this.props.onDeactivate, () => {});
+ rule: ruleDetails.key,
+ });
}
};
- handleRevert = (key?: string) => {
+ const handleRevert = (key?: string) => {
if (key !== undefined) {
activateRule({
key,
- rule: this.props.ruleDetails.key,
+ rule: ruleDetails.key,
reset: true,
- }).then(this.props.onActivate, () => {});
+ });
}
};
- renderInheritedProfile = (activation: RuleActivation, profile: Profile) => {
- if (!profile.parentName) {
- return null;
- }
- const profilePath = getQualityProfileUrl(profile.parentName, profile.language);
- return (
- (activation.inherit === 'OVERRIDES' || activation.inherit === 'INHERITED') && (
- <Note as="div" className="sw-flex sw-items-center sw-mt-2">
- <InheritanceIcon
- fill={activation.inherit === 'OVERRIDES' ? 'destructiveIconFocus' : 'currentColor'}
- />
- <DiscreetLink
- className="sw-ml-1"
- aria-label={`${translate('quality_profiles.parent')} ${profile.parentName}`}
- to={profilePath}
- >
- {profile.parentName}
- </DiscreetLink>
- </Note>
- )
- );
- };
-
- renderParameter = (param: { key: string; value: string }, parentActivation?: RuleActivation) => {
- const originalParam = parentActivation?.params.find((p) => p.key === param.key);
- const originalValue = originalParam?.value;
-
- return (
- <StyledParameter className="sw-my-4" key={param.key}>
- <span className="key">{param.key}</span>
- <span className="sep sw-mr-1">: </span>
- <span className="value" title={param.value}>
- {param.value}
- </span>
- {parentActivation && param.value !== originalValue && (
- <div className="sw-flex sw-ml-4">
- {translate('coding_rules.original')}
- <span className="value sw-ml-1" title={originalValue}>
- {originalValue}
- </span>
- </div>
- )}
- </StyledParameter>
- );
- };
-
- renderParameters = (activation: RuleActivation, parentActivation?: RuleActivation) => (
- <CellComponent>
- {activation.params.map((param) => this.renderParameter(param, parentActivation))}
- </CellComponent>
- );
-
- renderActions = (activation: RuleActivation, profile: Profile) => {
+ const renderRowActions = (activation: RuleActivation, profile: Profile) => {
const canEdit = profile.actions?.edit && !profile.isBuiltIn;
- const { ruleDetails } = this.props;
const hasParent = activation.inherit !== 'NONE' && profile.parentKey;
+
return (
<ActionCell>
{canEdit && (
ariaLabel={translateWithParameters('coding_rules.change_details_x', profile.name)}
buttonText={translate('change_verb')}
modalHeader={translate('coding_rules.change_details')}
- onDone={this.handleActivate}
+ onDone={props.onActivate}
profiles={[profile]}
rule={ruleDetails}
/>
)}
isDestructive
modalHeader={translate('coding_rules.revert_to_parent_definition')}
- onConfirm={this.handleRevert}
+ onConfirm={handleRevert}
>
{({ onClick }) => (
<DangerButtonSecondary className="sw-ml-2" onClick={onClick}>
</ConfirmButton>
)}
- {(!hasParent || this.props.canDeactivateInherited) && (
+ {(!hasParent || canDeactivateInherited) && (
<ConfirmButton
confirmButtonText={translate('yes')}
confirmData={profile.key}
modalBody={translate('coding_rules.deactivate.confirm')}
modalHeader={translate('coding_rules.deactivate')}
- onConfirm={this.handleDeactivate}
+ onConfirm={handleDeactivate}
>
{({ onClick }) => (
<DangerButtonSecondary
);
};
- renderActivation = (activation: RuleActivation) => {
- const { activations = [], ruleDetails } = this.props;
- const profile = this.props.referencedProfiles[activation.qProfile];
+ const renderActivationRow = (activation: RuleActivation) => {
+ const profile = referencedProfiles[activation.qProfile];
+
if (!profile) {
return null;
}
const parentActivation = activations.find((x) => x.qProfile === profile.parentKey);
+ const inheritedProfileSection = profile.parentName
+ ? (activation.inherit === 'OVERRIDES' || activation.inherit === 'INHERITED') && (
+ <Note as="div" className="sw-flex sw-items-center sw-mt-2">
+ <InheritanceIcon
+ fill={activation.inherit === 'OVERRIDES' ? 'destructiveIconFocus' : 'currentColor'}
+ />
+ <DiscreetLink
+ className="sw-ml-1"
+ aria-label={`${translate('quality_profiles.parent')} ${profile.parentName}`}
+ to={getQualityProfileUrl(profile.parentName, profile.language)}
+ >
+ {profile.parentName}
+ </DiscreetLink>
+ </Note>
+ )
+ : null;
+
return (
<TableRowInteractive key={profile.key}>
<ContentCell className="coding-rules-detail-quality-profile-name">
</Link>
{profile.isBuiltIn && <BuiltInQualityProfileBadge className="sw-ml-2" />}
</div>
- {this.renderInheritedProfile(activation, profile)}
+ {inheritedProfileSection}
</div>
</ContentCell>
- {!ruleDetails.templateKey && this.renderParameters(activation, parentActivation)}
- {this.renderActions(activation, profile)}
+ {!ruleDetails.templateKey && (
+ <CellComponent>
+ {activation.params.map((param: { key: string; value: string }) => {
+ const originalParam = parentActivation?.params.find((p) => p.key === param.key);
+ const originalValue = originalParam?.value;
+
+ return (
+ <StyledParameter className="sw-my-4" key={param.key}>
+ <span className="key">{param.key}</span>
+ <span className="sep sw-mr-1">: </span>
+ <span className="value" title={param.value}>
+ {param.value}
+ </span>
+ {parentActivation && param.value !== originalValue && (
+ <div className="sw-flex sw-ml-4">
+ {translate('coding_rules.original')}
+ <span className="value sw-ml-1" title={originalValue}>
+ {originalValue}
+ </span>
+ </div>
+ )}
+ </StyledParameter>
+ );
+ })}
+ </CellComponent>
+ )}
+ {renderRowActions(activation, profile)}
</TableRowInteractive>
);
};
+ return (
+ <div className="js-rule-profiles sw-mb-8">
+ <SubHeadingHighlight as="h2" id={PROFILES_HEADING_ID}>
+ <FormattedMessage id="coding_rules.quality_profiles" />
+ </SubHeadingHighlight>
- render() {
- const { activations = [], referencedProfiles, ruleDetails } = this.props;
- const canActivate = Object.values(referencedProfiles).some((profile) =>
- Boolean(profile.actions?.edit && profile.language === ruleDetails.lang),
- );
-
- return (
- <div className="js-rule-profiles sw-mb-8">
- <SubHeadingHighlight as="h2" id={PROFILES_HEADING_ID}>
- <FormattedMessage id="coding_rules.quality_profiles" />
- </SubHeadingHighlight>
-
- {canActivate && (
- <ActivationButton
- buttonText={translate('coding_rules.activate')}
- className="sw-mt-6"
- modalHeader={translate('coding_rules.activate_in_quality_profile')}
- onDone={this.handleActivate}
- profiles={filter(
- this.props.referencedProfiles,
- (profile) => !activations.find((activation) => activation.qProfile === profile.key),
- )}
- rule={ruleDetails}
- />
- )}
+ {canActivate && (
+ <ActivationButton
+ buttonText={translate('coding_rules.activate')}
+ className="sw-mt-6"
+ modalHeader={translate('coding_rules.activate_in_quality_profile')}
+ onDone={props.onActivate}
+ profiles={filter(
+ referencedProfiles,
+ (profile) => !activations.find((activation) => activation.qProfile === profile.key),
+ )}
+ rule={ruleDetails}
+ />
+ )}
- {activations.length > 0 && (
- <Table
- aria-labelledby={PROFILES_HEADING_ID}
- className="sw-my-6"
- columnCount={
- ruleDetails.templateKey ? COLUMN_COUNT_WITHOUT_PARAMS : COLUMN_COUNT_WITH_PARAMS
- }
- id="coding-rules-detail-quality-profiles"
- >
- {activations.map(this.renderActivation)}
- </Table>
- )}
- </div>
- );
- }
+ {activations.length > 0 && (
+ <Table
+ aria-labelledby={PROFILES_HEADING_ID}
+ className="sw-my-6"
+ columnCount={
+ ruleDetails.templateKey ? COLUMN_COUNT_WITHOUT_PARAMS : COLUMN_COUNT_WITH_PARAMS
+ }
+ id="coding-rules-detail-quality-profiles"
+ >
+ {activations.map(renderActivationRow)}
+ </Table>
+ )}
+ </div>
+ );
}
const StyledParameter = styled.div`
});
},
+ async detailsloaded() {
+ expect(await byRole('heading', { level: 1 }).find()).toBeInTheDocument();
+ await ui.appLoaded();
+ },
+
async bulkActivate(rulesCount: number, profile: Profile) {
await user.click(ui.bulkChangeButton.get());
await user.click(ui.activateIn.get());
return (
<div className="sw-mb-5">
<div className="sw-flex sw-h-9 sw-items-center sw-justify-between">
- <PageTitle className="sw-body-md-highlight" text={translate('filters')} />
+ <PageTitle className="sw-body-md-highlight" as="h2" text={translate('filters')} />
{displayReset && (
<DangerButtonSecondary onClick={onReset}>
*/
import classNames from 'classnames';
-import { cloneDeep, debounce, groupBy } from 'lodash';
+import { ToggleButton } from 'design-system';
+import { cloneDeep, debounce, groupBy, isEqual } from 'lodash';
import * as React from 'react';
import { Location } from 'react-router-dom';
import { dismissNotice } from '../../api/users';
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
import { translate } from '../../helpers/l10n';
-import { Issue, RuleDetails } from '../../types/types';
+import { RuleDetails } from '../../types/types';
import { NoticeType } from '../../types/users';
import { getTabId, getTabPanelId } from '../controls/BoxedTabs';
import withLocation from '../hoc/withLocation';
import MoreInfoRuleDescription from './MoreInfoRuleDescription';
import RuleDescription from './RuleDescription';
-
-import { ToggleButton } from 'design-system/lib';
import './style.css';
interface RuleTabViewerProps extends CurrentUserContextInterface {
ruleDetails: RuleDetails;
- extendedDescription?: string;
- ruleDescriptionContextKey?: string;
- activityTabContent?: React.ReactNode;
location: Location;
- selectedFlowIndex?: number;
- selectedLocationIndex?: number;
- issue?: Issue;
}
interface State {
}
componentDidUpdate(prevProps: RuleTabViewerProps, prevState: State) {
- const {
- ruleDetails,
- ruleDescriptionContextKey,
- currentUser,
- issue,
- selectedFlowIndex,
- selectedLocationIndex,
- } = this.props;
+ const { ruleDetails, currentUser } = this.props;
const { selectedTab } = this.state;
if (
- prevProps.ruleDetails.key !== ruleDetails.key ||
- prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey ||
- prevProps.issue !== issue ||
- prevProps.selectedFlowIndex !== selectedFlowIndex ||
- prevProps.selectedLocationIndex !== selectedLocationIndex ||
- prevProps.currentUser !== currentUser
+ !isEqual(prevProps.ruleDetails, ruleDetails) ||
+ !isEqual(prevProps.currentUser, currentUser)
) {
- this.setState((pState) =>
- this.computeState(
- pState,
- prevProps.ruleDetails !== ruleDetails ||
- (prevProps.issue && issue && prevProps.issue.key !== issue.key) ||
- prevProps.selectedFlowIndex !== selectedFlowIndex ||
- prevProps.selectedLocationIndex !== selectedLocationIndex,
- ),
- );
+ this.setState((pState) => this.computeState(pState, prevProps.ruleDetails !== ruleDetails));
}
if (selectedTab?.value === TabKeys.MoreInfo) {
computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
const {
ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType },
- extendedDescription,
- activityTabContent,
} = this.props;
// As we might tamper with the description later on, we clone to avoid any side effect
groupBy(descriptionSections, (section) => section.key),
);
- if (extendedDescription) {
- if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]?.length > 0) {
- // We add the extended description (htmlNote) in the first context, in case there are contexts
- // Extended description will get reworked in future
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
- '<br/>' + extendedDescription;
- } else {
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
- {
- content: extendedDescription,
- key: RuleDescriptionSections.RESOURCES,
- },
- ];
- }
- }
-
const tabs: Tab[] = [
{
content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
value: TabKeys.HowToFixIt,
label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
},
- {
- content: activityTabContent,
- value: TabKeys.Activity,
- label: translate('coding_rules.description_section.title', TabKeys.Activity),
- },
{
content: ((educationPrinciples && educationPrinciples.length > 0) ||
descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { UseQueryResult, useMutation, useQuery } from '@tanstack/react-query';
+import { UseQueryResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
+ ActivateRuleParameters,
AddRemoveGroupParameters,
AddRemoveUserParameters,
+ DeactivateRuleParameters,
Profile,
+ activateRule,
addGroup,
addUser,
compareProfiles,
+ deactivateRule,
getProfileInheritance,
} from '../api/quality-profiles';
import { ProfileInheritanceDetails } from '../types/types';
});
}
+export function useActivateRuleMutation(onSuccess: (data: ActivateRuleParameters) => unknown) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: activateRule,
+ onSuccess: (_, data) => {
+ queryClient.invalidateQueries({ queryKey: ['rules', 'details'] });
+ onSuccess(data);
+ },
+ });
+}
+
+export function useDeactivateRuleMutation(onSuccess: (data: DeactivateRuleParameters) => unknown) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: deactivateRule,
+ onSuccess: (_, data) => {
+ queryClient.invalidateQueries({ queryKey: ['rules', 'details'] });
+ onSuccess(data);
+ },
+ });
+}
+
export function useAddUserMutation(onSuccess: () => unknown) {
return useMutation({
mutationFn: (data: AddRemoveUserParameters) => addUser(data),
--- /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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { createRule, deleteRule, getRuleDetails, searchRules, updateRule } from '../api/rules';
+import { SearchRulesResponse } from '../types/coding-rules';
+import { SearchRulesQuery } from '../types/rules';
+import { RuleActivation, RuleDetails } from '../types/types';
+
+function getRulesQueryKey(type: 'search' | 'details', data?: SearchRulesQuery | string) {
+ const key = ['rules', type] as (string | SearchRulesQuery)[];
+ if (data) {
+ key.push(data);
+ }
+ return key;
+}
+
+export function useSearchRulesQuery(data: SearchRulesQuery) {
+ return useQuery({
+ queryKey: getRulesQueryKey('search', data),
+ queryFn: ({ queryKey: [, , query] }) => {
+ if (!query) {
+ return null;
+ }
+
+ return searchRules(data);
+ },
+ });
+}
+
+export function useRuleDetailsQuery(data: { key: string; actives?: boolean }) {
+ return useQuery({
+ queryKey: getRulesQueryKey('details', data.key),
+ queryFn: () => getRuleDetails(data),
+ });
+}
+
+export function useCreateRuleMutation(
+ searchQuery?: SearchRulesQuery,
+ onSuccess?: (rule: RuleDetails) => unknown,
+ onError?: (error: Response) => unknown,
+) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: createRule,
+ onError,
+ onSuccess: (rule) => {
+ onSuccess?.(rule);
+ queryClient.setQueryData<SearchRulesResponse>(
+ getRulesQueryKey('search', searchQuery),
+ (oldData) => {
+ return oldData ? { ...oldData, rules: [rule, ...oldData.rules] } : undefined;
+ },
+ );
+ },
+ });
+}
+
+export function useUpdateRuleMutation(onSuccess?: (rule: RuleDetails) => unknown) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: updateRule,
+ onSuccess: (rule) => {
+ onSuccess?.(rule);
+ queryClient.setQueryData<{ actives?: RuleActivation[]; rule: RuleDetails }>(
+ getRulesQueryKey('details', rule.key),
+ (oldData) => {
+ return {
+ ...oldData,
+ rule,
+ };
+ },
+ );
+ },
+ });
+}
+
+export function useDeleteRuleMutation(
+ searchQuery?: SearchRulesQuery,
+ onSuccess?: (key: string) => unknown,
+) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (params: { key: string }) => deleteRule(params),
+ onSuccess: (_, data) => {
+ onSuccess?.(data.key);
+ queryClient.setQueryData<SearchRulesResponse>(
+ getRulesQueryKey('search', searchQuery),
+ (oldData) => {
+ return oldData
+ ? { ...oldData, rules: oldData.rules.filter((rule) => rule.key !== data.key) }
+ : undefined;
+ },
+ );
+ },
+ });
+}