aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx')
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx256
1 files changed, 256 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
new file mode 100644
index 00000000000..54368071b6b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
@@ -0,0 +1,256 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import { Query, serializeQuery } from '../query';
+import { Profile, bulkActivateRules, bulkDeactivateRules } from '../../../api/quality-profiles';
+import Modal from '../../../components/controls/Modal';
+import Select from '../../../components/controls/Select';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+
+interface Props {
+ action: string;
+ onClose: () => void;
+ organization: string | undefined;
+ referencedProfiles: { [profile: string]: Profile };
+ profile?: Profile;
+ query: Query;
+ total: number;
+}
+
+interface ActivationResult {
+ failed: number;
+ profile: string;
+ succeeded: number;
+}
+
+interface State {
+ finished: boolean;
+ results: ActivationResult[];
+ selectedProfiles: any[];
+ submitting: boolean;
+}
+
+export default class BulkChangeModal extends React.PureComponent<Props, State> {
+ mounted: boolean;
+
+ constructor(props: Props) {
+ super(props);
+
+ // if there is only one possible option for profile, select it immediately
+ const selectedProfiles = [];
+ const availableProfiles = this.getAvailableQualityProfiles(props);
+ if (availableProfiles.length === 1) {
+ selectedProfiles.push(availableProfiles[0].key);
+ }
+
+ this.state = { finished: false, results: [], selectedProfiles, submitting: false };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleCloseClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onClose();
+ };
+
+ handleProfileSelect = (options: { value: string }[]) => {
+ const selectedProfiles = options.map(option => option.value);
+ this.setState({ selectedProfiles });
+ };
+
+ getAvailableQualityProfiles = ({ query, referencedProfiles } = this.props) => {
+ let profiles = Object.values(referencedProfiles);
+ if (query.languages.length > 0) {
+ profiles = profiles.filter(profile => query.languages.includes(profile.language));
+ }
+ return profiles
+ .filter(profile => profile.actions && profile.actions.edit)
+ .filter(profile => !profile.isBuiltIn);
+ };
+
+ processResponse = (profile: string, response: any) => {
+ if (this.mounted) {
+ const result: ActivationResult = {
+ failed: response.failed || 0,
+ profile,
+ succeeded: response.succeeded || 0
+ };
+ this.setState(state => ({ results: [...state.results, result] }));
+ }
+ };
+
+ sendRequests = () => {
+ let looper = Promise.resolve();
+
+ // serialize the query, but delete the `profile`
+ const data = serializeQuery(this.props.query);
+ delete data.profile;
+
+ const method = this.props.action === 'activate' ? bulkActivateRules : bulkDeactivateRules;
+
+ // if a profile is selected in the facet, pick it
+ // otherwise take all profiles selected in the dropdown
+ const profiles: string[] = this.props.profile
+ ? [this.props.profile.key]
+ : this.state.selectedProfiles;
+
+ for (const profile of profiles) {
+ looper = looper.then(() =>
+ method({ ...data, organization: this.props.organization, targetKey: profile }).then(
+ response => this.processResponse(profile, response)
+ )
+ );
+ }
+ return looper;
+ };
+
+ handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ this.setState({ submitting: true });
+ this.sendRequests().then(
+ () => {
+ if (this.mounted) {
+ this.setState({ finished: true, submitting: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ }
+ );
+ };
+
+ renderResult = (result: ActivationResult) => {
+ const { profile: profileKey } = result;
+ const profile = this.props.referencedProfiles[profileKey];
+ if (!profile) {
+ return null;
+ }
+ return (
+ <div
+ className={classNames('alert', {
+ 'alert-warning': result.failed > 0,
+ 'alert-success': result.failed === 0
+ })}
+ key={result.profile}>
+ {result.failed
+ ? translateWithParameters(
+ 'coding_rules.bulk_change.warning',
+ profile.name,
+ profile.language,
+ result.succeeded,
+ result.failed
+ )
+ : translateWithParameters(
+ 'coding_rules.bulk_change.success',
+ profile.name,
+ profile.language,
+ result.succeeded
+ )}
+ </div>
+ );
+ };
+
+ renderProfileSelect = () => {
+ const profiles = this.getAvailableQualityProfiles();
+ const options = profiles.map(profile => ({
+ label: `${profile.name} - ${profile.languageName}`,
+ value: profile.key
+ }));
+ return (
+ <Select
+ multi={true}
+ onChange={this.handleProfileSelect}
+ options={options}
+ value={this.state.selectedProfiles}
+ />
+ );
+ };
+
+ render() {
+ const { action, profile, total } = this.props;
+ const header =
+ // prettier-ignore
+ action === 'activate'
+ ? `${translate('coding_rules.activate_in_quality_profile')} (${formatMeasure(total, 'INT')} ${translate('coding_rules._rules')})`
+ : `${translate('coding_rules.deactivate_in_quality_profile')} (${formatMeasure(total, 'INT')} ${translate('coding_rules._rules')})`;
+
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <form onSubmit={this.handleFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+
+ <div className="modal-body">
+ {this.state.results.map(this.renderResult)}
+
+ {!this.state.finished &&
+ !this.state.submitting && (
+ <div className="modal-field">
+ <h3>
+ <label htmlFor="coding-rules-bulk-change-profile">
+ {action === 'activate'
+ ? translate('coding_rules.activate_in')
+ : translate('coding_rules.deactivate_in')}
+ </label>
+ </h3>
+ {profile ? (
+ <h3 className="readonly-field">
+ {profile.name}
+ {' — '}
+ {translate('are_you_sure')}
+ </h3>
+ ) : (
+ this.renderProfileSelect()
+ )}
+ </div>
+ )}
+ </div>
+
+ <footer className="modal-foot">
+ {this.state.submitting && <i className="spinner spacer-right" />}
+ {!this.state.finished && (
+ <button
+ disabled={this.state.submitting}
+ id="coding-rules-submit-bulk-change"
+ type="submit">
+ {translate('apply')}
+ </button>
+ )}
+ <button className="button-link" onClick={this.handleCloseClick} type="reset">
+ {this.state.finished ? translate('close') : translate('cancel')}
+ </button>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}