You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CustomRuleFormModal.tsx 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import * as React from 'react';
  21. import { csvEscape } from 'sonar-ui-common/helpers/csv';
  22. import { latinize } from 'sonar-ui-common/helpers/strings';
  23. import { translate } from 'sonar-ui-common/helpers/l10n';
  24. import { SubmitButton, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
  25. import Modal from 'sonar-ui-common/components/controls/Modal';
  26. import { Alert } from 'sonar-ui-common/components/ui/Alert';
  27. import Select from 'sonar-ui-common/components/controls/Select';
  28. import MarkdownTips from '../../../components/common/MarkdownTips';
  29. import SeverityHelper from '../../../components/shared/SeverityHelper';
  30. import TypeHelper from '../../../components/shared/TypeHelper';
  31. import { createRule, updateRule } from '../../../api/rules';
  32. import { SEVERITIES, RULE_TYPES, RULE_STATUSES } from '../../../helpers/constants';
  33. interface Props {
  34. customRule?: T.RuleDetails;
  35. onClose: () => void;
  36. onDone: (newRuleDetails: T.RuleDetails) => void;
  37. organization: string | undefined;
  38. templateRule: T.RuleDetails;
  39. }
  40. interface State {
  41. description: string;
  42. key: string;
  43. keyModifiedByUser: boolean;
  44. name: string;
  45. params: T.Dict<string>;
  46. reactivating: boolean;
  47. severity: string;
  48. status: string;
  49. submitting: boolean;
  50. type: string;
  51. }
  52. export default class CustomRuleFormModal extends React.PureComponent<Props, State> {
  53. mounted = false;
  54. constructor(props: Props) {
  55. super(props);
  56. const params: T.Dict<string> = {};
  57. if (props.customRule && props.customRule.params) {
  58. for (const param of props.customRule.params) {
  59. params[param.key] = param.defaultValue || '';
  60. }
  61. }
  62. this.state = {
  63. description: (props.customRule && props.customRule.mdDesc) || '',
  64. key: '',
  65. keyModifiedByUser: false,
  66. name: (props.customRule && props.customRule.name) || '',
  67. params,
  68. reactivating: false,
  69. severity: (props.customRule && props.customRule.severity) || props.templateRule.severity,
  70. status: (props.customRule && props.customRule.status) || props.templateRule.status,
  71. submitting: false,
  72. type: (props.customRule && props.customRule.type) || props.templateRule.type
  73. };
  74. }
  75. componentDidMount() {
  76. this.mounted = true;
  77. }
  78. componentWillUnmount() {
  79. this.mounted = false;
  80. }
  81. prepareRequest = () => {
  82. const { customRule, organization, templateRule } = this.props;
  83. const params = Object.keys(this.state.params)
  84. .map(key => `${key}=${csvEscape(this.state.params[key])}`)
  85. .join(';');
  86. const ruleData = {
  87. markdown_description: this.state.description,
  88. name: this.state.name,
  89. organization,
  90. params,
  91. severity: this.state.severity,
  92. status: this.state.status
  93. };
  94. return customRule
  95. ? updateRule({ ...ruleData, key: customRule.key })
  96. : createRule({
  97. ...ruleData,
  98. custom_key: this.state.key,
  99. prevent_reactivation: !this.state.reactivating,
  100. template_key: templateRule.key,
  101. type: this.state.type
  102. });
  103. };
  104. handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
  105. event.preventDefault();
  106. this.setState({ submitting: true });
  107. this.prepareRequest().then(
  108. newRuleDetails => {
  109. if (this.mounted) {
  110. this.setState({ submitting: false });
  111. this.props.onDone(newRuleDetails);
  112. }
  113. },
  114. (response: Response) => {
  115. if (this.mounted) {
  116. this.setState({ reactivating: response.status === 409, submitting: false });
  117. }
  118. }
  119. );
  120. };
  121. handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
  122. const { value: name } = event.currentTarget;
  123. this.setState(state => ({
  124. name,
  125. key: state.keyModifiedByUser ? state.key : latinize(name).replace(/[^A-Za-z0-9]/g, '_')
  126. }));
  127. };
  128. handleKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
  129. this.setState({ key: event.currentTarget.value, keyModifiedByUser: true });
  130. handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) =>
  131. this.setState({ description: event.currentTarget.value });
  132. handleTypeChange = ({ value }: { value: string }) => this.setState({ type: value });
  133. handleSeverityChange = ({ value }: { value: string }) => this.setState({ severity: value });
  134. handleStatusChange = ({ value }: { value: string }) => this.setState({ status: value });
  135. handleParameterChange = (event: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  136. const { name, value } = event.currentTarget;
  137. this.setState((state: State) => ({ params: { ...state.params, [name]: value } }));
  138. };
  139. renderNameField = () => (
  140. <div className="modal-field">
  141. <label htmlFor="coding-rules-custom-rule-creation-name">
  142. {translate('name')} <em className="mandatory">*</em>
  143. </label>
  144. <input
  145. autoFocus={true}
  146. disabled={this.state.submitting}
  147. id="coding-rules-custom-rule-creation-name"
  148. onChange={this.handleNameChange}
  149. required={true}
  150. type="text"
  151. value={this.state.name}
  152. />
  153. </div>
  154. );
  155. renderKeyField = () => (
  156. <div className="modal-field">
  157. <label htmlFor="coding-rules-custom-rule-creation-key">
  158. {translate('key')} {!this.props.customRule && <em className="mandatory">*</em>}
  159. </label>
  160. {this.props.customRule ? (
  161. <span className="coding-rules-detail-custom-rule-key" title={this.props.customRule.key}>
  162. {this.props.customRule.key}
  163. </span>
  164. ) : (
  165. <input
  166. disabled={this.state.submitting}
  167. id="coding-rules-custom-rule-creation-key"
  168. onChange={this.handleKeyChange}
  169. required={true}
  170. type="text"
  171. value={this.state.key}
  172. />
  173. )}
  174. </div>
  175. );
  176. renderDescriptionField = () => (
  177. <div className="modal-field">
  178. <label htmlFor="coding-rules-custom-rule-creation-html-description">
  179. {translate('description')} <em className="mandatory">*</em>
  180. </label>
  181. <textarea
  182. disabled={this.state.submitting}
  183. id="coding-rules-custom-rule-creation-html-description"
  184. onChange={this.handleDescriptionChange}
  185. required={true}
  186. rows={5}
  187. value={this.state.description}
  188. />
  189. <MarkdownTips className="modal-field-descriptor text-right" />
  190. </div>
  191. );
  192. renderTypeOption = ({ value }: { value: T.RuleType }) => {
  193. return <TypeHelper type={value} />;
  194. };
  195. renderTypeField = () => (
  196. <div className="modal-field flex-1 spacer-right">
  197. <label htmlFor="coding-rules-custom-rule-type">{translate('type')}</label>
  198. <Select
  199. clearable={false}
  200. disabled={this.state.submitting}
  201. id="coding-rules-custom-rule-type"
  202. onChange={this.handleTypeChange}
  203. optionRenderer={this.renderTypeOption}
  204. options={RULE_TYPES.map(type => ({
  205. label: translate('issue.type', type),
  206. value: type
  207. }))}
  208. searchable={false}
  209. value={this.state.type}
  210. valueRenderer={this.renderTypeOption}
  211. />
  212. </div>
  213. );
  214. renderSeverityOption = ({ value }: { value: string }) => <SeverityHelper severity={value} />;
  215. renderSeverityField = () => (
  216. <div className="modal-field flex-1 spacer-right">
  217. <label htmlFor="coding-rules-custom-rule-severity">{translate('severity')}</label>
  218. <Select
  219. clearable={false}
  220. disabled={this.state.submitting}
  221. id="coding-rules-custom-rule-severity"
  222. onChange={this.handleSeverityChange}
  223. optionRenderer={this.renderSeverityOption}
  224. options={SEVERITIES.map(severity => ({
  225. label: translate('severity', severity),
  226. value: severity
  227. }))}
  228. searchable={false}
  229. value={this.state.severity}
  230. valueRenderer={this.renderSeverityOption}
  231. />
  232. </div>
  233. );
  234. renderStatusField = () => (
  235. <div className="modal-field flex-1">
  236. <label htmlFor="coding-rules-custom-rule-status">
  237. {translate('coding_rules.filters.status')}
  238. </label>
  239. <Select
  240. clearable={false}
  241. disabled={this.state.submitting}
  242. id="coding-rules-custom-rule-status"
  243. onChange={this.handleStatusChange}
  244. options={RULE_STATUSES.map(status => ({
  245. label: translate('rules.status', status),
  246. value: status
  247. }))}
  248. searchable={false}
  249. value={this.state.status}
  250. />
  251. </div>
  252. );
  253. renderParameterField = (param: T.RuleParameter) => (
  254. <div className="modal-field" key={param.key}>
  255. <label className="capitalize" htmlFor={param.key}>
  256. {param.key}
  257. </label>
  258. {param.type === 'TEXT' ? (
  259. <textarea
  260. disabled={this.state.submitting}
  261. id={param.key}
  262. name={param.key}
  263. onChange={this.handleParameterChange}
  264. placeholder={param.defaultValue}
  265. rows={3}
  266. value={this.state.params[param.key] || ''}
  267. />
  268. ) : (
  269. <input
  270. disabled={this.state.submitting}
  271. id={param.key}
  272. name={param.key}
  273. onChange={this.handleParameterChange}
  274. placeholder={param.defaultValue}
  275. type="text"
  276. value={this.state.params[param.key] || ''}
  277. />
  278. )}
  279. <div
  280. className="modal-field-description"
  281. // Safe: defined by rule creator (instance admin?)
  282. dangerouslySetInnerHTML={{ __html: param.htmlDesc || '' }}
  283. />
  284. </div>
  285. );
  286. render() {
  287. const { customRule, templateRule } = this.props;
  288. const { reactivating, submitting } = this.state;
  289. const { params = [] } = templateRule;
  290. const header = customRule
  291. ? translate('coding_rules.update_custom_rule')
  292. : translate('coding_rules.create_custom_rule');
  293. let submit = this.props.customRule ? translate('save') : translate('create');
  294. if (this.state.reactivating) {
  295. submit = translate('coding_rules.reactivate');
  296. }
  297. return (
  298. <Modal contentLabel={header} onRequestClose={this.props.onClose}>
  299. <form onSubmit={this.handleFormSubmit}>
  300. <div className="modal-head">
  301. <h2>{header}</h2>
  302. </div>
  303. <div className="modal-body modal-container">
  304. {reactivating && (
  305. <Alert variant="warning">{translate('coding_rules.reactivate.help')}</Alert>
  306. )}
  307. {this.renderNameField()}
  308. {this.renderKeyField()}
  309. <div className="display-flex-space-between">
  310. {/* do not allow to change the type of existing rule */}
  311. {!customRule && this.renderTypeField()}
  312. {this.renderSeverityField()}
  313. {this.renderStatusField()}
  314. </div>
  315. {this.renderDescriptionField()}
  316. {params.map(this.renderParameterField)}
  317. </div>
  318. <div className="modal-foot">
  319. {submitting && <i className="spinner spacer-right" />}
  320. <SubmitButton disabled={this.state.submitting}>{submit}</SubmitButton>
  321. <ResetButtonLink
  322. disabled={submitting}
  323. id="coding-rules-custom-rule-creation-cancel"
  324. onClick={this.props.onClose}>
  325. {translate('cancel')}
  326. </ResetButtonLink>
  327. </div>
  328. </form>
  329. </Modal>
  330. );
  331. }
  332. }