diff --git a/server/sonar-web/src/main/js/@types/svg.d.ts b/server/sonar-web/src/main/js/@types/svg.d.ts new file mode 100644 index 000000000000..53cd43019be8 --- /dev/null +++ b/server/sonar-web/src/main/js/@types/svg.d.ts @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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. + */ + +declare module '*.svg' { + const content: string; + export default content; +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ai-custom-rules/AIRuleFormModal.css b/server/sonar-web/src/main/js/apps/coding-rules/components/ai-custom-rules/AIRuleFormModal.css new file mode 100644 index 000000000000..56a5d0713989 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ai-custom-rules/AIRuleFormModal.css @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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. + */ +.ai-rule-form-body { + max-width: 900px; +} + +.ai-rule-form-alert { + line-height: 1.6; +} + +.ai-rule-form-alert p { + margin: 8px 0 0 0; + color: #666; +} + +.form-field-error { + color: #d4333f; + font-size: 12px; + margin-top: 4px; +} + +.ai-examples-section { + background: #f5f5f5; + padding: 16px; + border-radius: 4px; + margin-top: 16px; +} + +.ai-examples-list { + margin: 8px 0 0 20px; + padding: 0; + list-style-type: none; +} + +.ai-examples-list li { + margin-bottom: 8px; + line-height: 1.6; + color: #666; +} + +.ai-examples-list li:before { + content: "•"; + margin-right: 8px; + color: #4b9fd5; +} + +.ai-form-actions { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ai-custom-rules/AIRuleFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ai-custom-rules/AIRuleFormModal.tsx new file mode 100644 index 000000000000..0a0429e2e9a6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ai-custom-rules/AIRuleFormModal.tsx @@ -0,0 +1,259 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { Button, ButtonVariety, Modal, ModalSize, Select, Text } from '@sonarsource/echoes-react'; +import { SyntheticEvent, useState } from 'react'; +import { FormField, InputField, InputTextArea, LabelValueSelectOption } from '~design-system'; +import { RULE_TYPES } from '../../../../helpers/constants'; +import { translate } from '../../../../helpers/l10n'; +import { latinize } from '../../../../helpers/strings'; +import { SeveritySelect } from '../SeveritySelect'; +import SparklesSvg from './icons/Sparkles-2.svg'; + +interface Props { + isOpen: boolean; + onClose: () => void; + onSubmit: (values: AIRuleFormValues) => void; + organization: string; +} + +export interface AIRuleFormValues { + ruleName: string; + ruleKey: string; + ruleType: string; + severity: string; + message: string; + ruleDescription: string; +} + +const MAX_CHAR_COUNT = 5000; +const MIN_CHAR_COUNT = 20; + +const EXAMPLE_DESCRIPTIONS = [ + 'SOQLs cannot be placed too close to any for loop', + 'Triggers must delegate to business logic to implement logic', + 'Apex classes should not exceed 1000 lines of code', + 'Apex class handling must not use the identified Apex/Logger framework', +]; + +export default function AIRuleFormModal(props: Readonly) { + const { isOpen, onClose, onSubmit } = props; + const [ruleName, setRuleName] = useState(''); + const [ruleKey, setRuleKey] = useState(''); + const [keyModifiedByUser, setKeyModifiedByUser] = useState(false); + const [ruleType, setRuleType] = useState('CODE_SMELL'); + const [severity, setSeverity] = useState('MAJOR'); + const [message, setMessage] = useState(''); + const [ruleDescription, setRuleDescription] = useState(''); + const [charCount, setCharCount] = useState(0); + + const handleSubmit = () => { + if (validateForm()) { + onSubmit({ + ruleName, + ruleKey, + ruleType, + severity, + message, + ruleDescription, + }); + } + }; + + const validateForm = (): boolean => { + return ( + ruleName.trim().length > 0 && + ruleKey.trim().length > 0 && + ruleType.length > 0 && + severity.length > 0 && + message.trim().length > 0 && + ruleDescription.trim().length >= MIN_CHAR_COUNT && + ruleDescription.trim().length <= MAX_CHAR_COUNT + ); + }; + + const typeOptions: LabelValueSelectOption[] = RULE_TYPES.map((type) => ({ + label: translate('issue.type', type), + value: type, + })); + + return ( + +
+ + AI Rule Generation +
+ Describe your coding standard in plain English. The AI will analyze your description + and automatically generate the rule syntax. +
+
+ + + ) => { + setRuleName(value); + if (!keyModifiedByUser) { + const generatedKey = latinize(value) + .replace(/[^A-Za-z0-9]/g, '_'); + setRuleKey(generatedKey); + } + }} + placeholder="e.g., Avoid Complex Methods" + size="full" + type="text" + value={ruleName} + /> + + + + + + +
+
+ + handleNameChange(e.target.value)} + autoFocus + /> + {submitted && !ruleName.trim() && ( +
+ + Rule name is required. +
+ )} +
+
+
+ + Rule Key* + + Auto-generated from name +
+ + {submitted && !ruleKey.trim() && ( +
+ + Rule key is required. +
+ )} +
+
+ + {/* Row 2: Type + Severity */} +
+
+
+ Rule Type +
+ value && setSeverity(value)} + data={SEVERITIES.map((sev) => ({ + label: sev.label, + value: sev.value, + prefix: , + }))} + value={severity} + valueIcon={} + /> +
+
+ + {/* Row 3: Message */} +
+
+ Message + Shown in violation reports +
+ setMessage(e.target.value)} + /> +
+ + {/* Row 4: Description */} +
+
+ + Rule Description* + + {charCount} characters +
+