diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-08-10 18:16:54 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-08-10 18:16:54 +0200 |
commit | c12f3d56cb99478c36242fd122258ebf0d26f740 (patch) | |
tree | 2a7cae9094a9773bf5ba621bb3214f52c2c6126d /server | |
parent | 6d55c79eed10aefdda9381cb82acdac47c824ec6 (diff) | |
download | sonarqube-c12f3d56cb99478c36242fd122258ebf0d26f740.tar.gz sonarqube-c12f3d56cb99478c36242fd122258ebf0d26f740.zip |
SONAR-7919 Rewrite "Update Key" project page (#1140)
Diffstat (limited to 'server')
22 files changed, 960 insertions, 231 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index f0f49a636b4..6b3238022fb 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -109,3 +109,29 @@ export function getMyProjects (data) { const url = '/api/projects/search_my_projects'; return getJSON(url, data); } + +/** + * Change component's key + * @param {string} key + * @param {string} newKey + * @returns {Promise} + */ +export function changeKey (key, newKey) { + const url = '/api/components/update_key'; + const data = { key, newKey }; + return post(url, data); +} + +/** + * Bulk change component's key + * @param {string} key + * @param {string} from + * @param {string} to + * @param {boolean} dryRun + * @returns {Promise} + */ +export function bulkChangeKey (key, from, to, dryRun = false) { + const url = '/api/components/bulk_update_key'; + const data = { key, from, to, dryRun }; + return postJSON(url, data); +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/app.js b/server/sonar-web/src/main/js/apps/project-admin/app.js index 7981c936e87..903ed046cb9 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/app.js +++ b/server/sonar-web/src/main/js/apps/project-admin/app.js @@ -26,6 +26,7 @@ import Deletion from './deletion/Deletion'; import QualityProfiles from './quality-profiles/QualityProfiles'; import QualityGate from './quality-gate/QualityGate'; import Links from './links/Links'; +import Key from './key/Key'; import rootReducer from './store/rootReducer'; import configureStore from '../../components/store/configureStore'; @@ -56,6 +57,9 @@ window.sonarqube.appStarted.then(options => { <Route path="/links" component={withComponent(Links)}/> + <Route + path="/key" + component={withComponent(Key)}/> </Router> </Provider> ), el); diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js new file mode 100644 index 00000000000..1624faddc3f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js @@ -0,0 +1,127 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import BulkUpdateForm from './BulkUpdateForm'; +import BulkUpdateResults from './BulkUpdateResults'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { bulkChangeKey } from '../../../api/components'; +import { getComponentUrl } from '../../../helpers/urls'; + +export default class BulkUpdate extends React.Component { + static propTypes = { + component: React.PropTypes.object.isRequired + }; + + state = { + updating: false, + updated: false, + newComponentKey: null + }; + + handleSubmit (replace, by) { + this.loadResults(replace, by); + } + + handleConfirm () { + this.setState({ updating: true }); + + const { component } = this.props; + const { replace, by } = this.state; + bulkChangeKey(component.key, replace, by).then(r => { + const result = r.keys.find(result => result.key === component.key); + const newComponentKey = result != null ? result.newKey : component.key; + this.setState({ + updating: false, + updated: true, + newComponentKey + }); + }); + } + + loadResults (replace, by) { + const { component } = this.props; + bulkChangeKey(component.key, replace, by, true).then(r => { + this.setState({ results: r.keys, replace, by }); + }); + } + + renderUpdating () { + return ( + <div id="project-key-bulk-update"> + <i className="spinner"/> + </div> + ); + } + + renderUpdated () { + return ( + <div id="project-key-bulk-update"> + <div className="alert alert-success"> + {translate('update_key.key_updated')} + {' '} + <a href={getComponentUrl(this.state.newComponentKey)}> + {translate('check_project')} + </a> + </div> + </div> + ); + } + + render () { + const { component } = this.props; + const { updating, updated } = this.state; + const { results, replace, by } = this.state; + + if (updating) { + return this.renderUpdating(); + } + + if (updated) { + return this.renderUpdated(); + } + + return ( + <div id="project-key-bulk-update"> + <header className="big-spacer-bottom"> + <div className="spacer-bottom"> + {translate('update_key.bulk_change_description')} + </div> + <div> + {translateWithParameters( + 'update_key.current_key_for_project_x_is_x', + component.name, + component.key + )} + </div> + </header> + + <BulkUpdateForm onSubmit={this.handleSubmit.bind(this)}/> + + {results != null && ( + <BulkUpdateResults + results={results} + replace={replace} + by={by} + onConfirm={this.handleConfirm.bind(this)}/> + )} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js new file mode 100644 index 00000000000..db31d98063d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default class BulkUpdateForm extends React.Component { + static propTypes = { + onSubmit: React.PropTypes.func.isRequired + }; + + handleSubmit (e) { + e.preventDefault(); + this.refs.submit.blur(); + + const replace = this.refs.replace.value; + const by = this.refs.by.value; + + this.props.onSubmit(replace, by); + } + + render () { + return ( + <form onSubmit={this.handleSubmit.bind(this)}> + <div className="modal-field"> + <label htmlFor="bulk-update-replace"> + {translate('update_key.replace')} + </label> + <input + ref="replace" + id="bulk-update-replace" + name="replace" + type="text" + placeholder={translate('update_key.replace_example')} + required/> + </div> + + <div className="modal-field"> + <label htmlFor="bulk-update-by"> + {translate('update_key.by')} + </label> + <input + ref="by" + id="bulk-update-by" + name="by" + type="text" + placeholder={translate('update_key.by_example')} + required/> + <button + ref="submit" + id="bulk-update-see-results" + className="big-spacer-left"> + {translate('update_key.see_results')} + </button> + </div> + </form> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js new file mode 100644 index 00000000000..56f5531875b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js @@ -0,0 +1,111 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import some from 'lodash/some'; +import { translateWithParameters, translate } from '../../../helpers/l10n'; + +export default class BulkUpdateResults extends React.Component { + static propTypes = { + results: React.PropTypes.array.isRequired, + onConfirm: React.PropTypes.func.isRequired + }; + + handleConfirm (e) { + e.preventDefault(); + e.target.blur(); + this.props.onConfirm(); + } + + render () { + const { results, replace, by } = this.props; + const isEmpty = results.length === 0; + const hasDuplications = some(results, r => r.duplicate); + const canUpdate = !isEmpty && !hasDuplications; + + return ( + <div id="bulk-update-simulation" className="big-spacer-top"> + {isEmpty && ( + <div id="bulk-update-nothing" className="alert alert-warning"> + {translateWithParameters( + 'update_key.no_key_to_update', + replace + )} + </div> + )} + + {hasDuplications && ( + <div id="bulk-update-duplicate" className="alert alert-danger"> + {translateWithParameters( + 'update_key.cant_update_because_duplicate_keys', + replace, + by + )} + </div> + )} + + {canUpdate && ( + <div className="spacer-bottom"> + {translate('update_key.keys_will_be_updated_as_follows')} + </div> + )} + + {!isEmpty && ( + <table + id="bulk-update-results" + className="data zebra zebra-hover"> + <thead> + <tr> + <th>{translate('update_key.old_key')}</th> + <th>{translate('update_key.new_key')}</th> + </tr> + </thead> + <tbody> + {results.map(result => ( + <tr key={result.key} data-key={result.key}> + <td className="js-old-key"> + {result.key} + </td> + <td className="js-new-key"> + {result.duplicate && ( + <span className="spacer-right badge badge-danger"> + {translate('update_key.duplicate_key')} + </span> + )} + {result.newKey} + </td> + </tr> + ))} + </tbody> + </table> + )} + + <div className="big-spacer-top"> + {canUpdate && ( + <button + id="bulk-update-confirm" + onClick={this.handleConfirm.bind(this)}> + {translate('update_verb')} + </button> + )} + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js new file mode 100644 index 00000000000..6f7deacf0ed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import UpdateKeyForm from './UpdateKeyForm'; +import QualifierIcon from '../../../components/shared/qualifier-icon'; +import { translate } from '../../../helpers/l10n'; + +export default class FineGrainedUpdate extends React.Component { + render () { + const { component, modules } = this.props; + const components = [component, ...modules]; + + return ( + <div id="project-key-fine-grained-update"> + <table className="data zebra"> + <tbody> + {components.map(component => ( + <tr key={component.key}> + <td className="width-40"> + <QualifierIcon qualifier={component.qualifier}/> + {' '} + {component.name} + </td> + <td> + <UpdateKeyForm + component={component} + onKeyChange={this.props.onKeyChange}/> + </td> + </tr> + ))} + </tbody> + </table> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Header.js b/server/sonar-web/src/main/js/apps/project-admin/key/Header.js new file mode 100644 index 00000000000..c730528a8a2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/Header.js @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default class Header extends React.Component { + render () { + return ( + <header className="page-header"> + <h1 className="page-title"> + {translate('update_key.page')} + </h1> + <div className="page-description"> + {translate('update_key.page.description')} + </div> + </header> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js new file mode 100644 index 00000000000..47b61bc3e6e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js @@ -0,0 +1,134 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { connect } from 'react-redux'; +import Header from './Header'; +import UpdateForm from './UpdateForm'; +import BulkUpdate from './BulkUpdate'; +import FineGrainedUpdate from './FineGrainedUpdate'; +import { getProjectModules } from '../store/rootReducer'; +import { fetchProjectModules, changeKey } from '../store/actions'; +import { translate } from '../../../helpers/l10n'; + +class Key extends React.Component { + static propTypes = { + component: React.PropTypes.object.isRequired, + fetchProjectModules: React.PropTypes.func.isRequired, + changeKey: React.PropTypes.func.isRequired + }; + + state = { + tab: 'bulk' + }; + + componentDidMount () { + this.props.fetchProjectModules(this.props.component.key); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleChangeKey (key, newKey) { + return this.props.changeKey(key, newKey).then(() => { + if (key === this.props.component.key) { + window.location = window.baseUrl + + '/project/key?id=' + encodeURIComponent(newKey); + } + }); + } + + handleChangeTab (tab, e) { + e.preventDefault(); + e.target.blur(); + this.setState({ tab }); + } + + render () { + const { component, modules } = this.props; + + const noModules = modules != null && modules.length === 0; + const hasModules = modules != null && modules.length > 0; + + const { tab } = this.state; + + return ( + <div id="project-key" className="page page-limited"> + <Header/> + + {modules == null && ( + <i className="spinner"/> + )} + + {noModules && ( + <UpdateForm + component={component} + onKeyChange={this.handleChangeKey.bind(this)}/> + )} + + {hasModules && ( + <div> + <div className="big-spacer-bottom"> + <ul className="tabs"> + <li> + <a id="update-key-tab-bulk" + className={tab === 'bulk' ? 'selected' : ''} + href="#" + onClick={this.handleChangeTab.bind(this, 'bulk')}> + {translate('update_key.bulk_update')} + </a> + </li> + <li> + <a id="update-key-tab-fine" + className={tab === 'fine' ? 'selected' : ''} + href="#" + onClick={this.handleChangeTab.bind(this, 'fine')}> + {translate('update_key.fine_grained_key_update')} + </a> + </li> + </ul> + </div> + + {tab === 'bulk' && ( + <BulkUpdate component={component}/> + )} + + {tab === 'fine' && ( + <FineGrainedUpdate + component={component} + modules={modules} + onKeyChange={this.handleChangeKey.bind(this)}/> + )} + </div> + )} + </div> + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + modules: getProjectModules(state, ownProps.component.key) +}); + +export default connect( + mapStateToProps, + { fetchProjectModules, changeKey } +)(Key); diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js new file mode 100644 index 00000000000..701d2070957 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import UpdateKeyConfirmation from './views/UpdateKeyConfirmation'; +import { translate } from '../../../helpers/l10n'; + +export default class UpdateForm extends React.Component { + static propTypes = { + component: React.PropTypes.object.isRequired, + onKeyChange: React.PropTypes.func.isRequired + }; + + state = { newKey: null }; + + handleSubmit (e) { + e.preventDefault(); + + const newKey = this.refs.newKey.value; + + new UpdateKeyConfirmation({ + newKey, + component: this.props.component, + onChange: this.props.onKeyChange + }).render(); + } + + handleChange (e) { + const newKey = e.target.value; + this.setState({ newKey }); + } + + render () { + const value = this.state.newKey != null ? + this.state.newKey : + this.props.component.key; + + const hasChanged = value !== this.props.component.key; + + return ( + <form onSubmit={this.handleSubmit.bind(this)}> + <input + ref="newKey" + id="update-key-new-key" + className="input-super-large" + value={value} + type="text" + placeholder={translate('update_key.new_key')} + required + onChange={this.handleChange.bind(this)}/> + + <div className="spacer-top"> + <button id="update-key-submit" disabled={!hasChanged}> + {translate('update_verb')} + </button> + </div> + </form> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js new file mode 100644 index 00000000000..f23d1f7069e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import UpdateKeyConfirmation from './views/UpdateKeyConfirmation'; +import { translate } from '../../../helpers/l10n'; + +export default class UpdateKeyForm extends React.Component { + static propTypes = { + component: React.PropTypes.object.isRequired + }; + + state = {}; + + componentWillMount () { + this.handleInputChange = this.handleInputChange.bind(this); + this.handleUpdateClick = this.handleUpdateClick.bind(this); + this.handleResetClick = this.handleResetClick.bind(this); + } + + handleInputChange (e) { + const key = e.target.value; + this.setState({ key }); + } + + handleUpdateClick (e) { + e.preventDefault(); + e.target.blur(); + + const newKey = this.refs.newKey.value; + + new UpdateKeyConfirmation({ + newKey, + component: this.props.component, + onChange: this.props.onKeyChange + }).render(); + } + + handleResetClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ key: null }); + } + + render () { + const { component } = this.props; + + const value = this.state.key != null ? + this.state.key : + component.key; + + const hasChanged = this.state.key != null && + this.state.key !== component.key; + + return ( + <div className="js-fine-grained-update" data-key={component.key}> + <input + ref="newKey" + className="input-super-large big-spacer-right" + type="text" + value={value} + onChange={this.handleInputChange}/> + + <div className="button-group"> + <button disabled={!hasChanged} onClick={this.handleUpdateClick}> + {translate('update_verb')} + </button> + + <button disabled={!hasChanged} onClick={this.handleResetClick}> + {translate('reset_verb')} + </button> + </div> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs new file mode 100644 index 00000000000..bec83b7693e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs @@ -0,0 +1,30 @@ +<form id="update-key-confirmation-form" autocomplete="off"> + <div class="modal-head"> + <h2>{{t 'update_key.page'}}</h2> + </div> + <div class="modal-body"> + <div class="js-modal-messages"></div> + {{tp 'update_key.are_you_sure_to_change_key' component.name}} + <div class="spacer-top"> + <div class="display-inline-block text-right" style="width: 80px;"> + {{t 'update_key.old_key'}}: + </div> + <div class="display-inline-block"> + {{component.key}} + </div> + </div> + <div class="spacer-top"> + <div class="display-inline-block text-right" style="width: 80px;"> + {{t 'update_key.new_key'}}: + </div> + <div class="display-inline-block"> + {{newKey}} + </div> + </div> + </div> + <div class="modal-foot"> + <i class="js-modal-spinner spinner spacer-right hidden"></i> + <button id="update-key-confirm">{{t 'update_verb'}}</button> + <a href="#" class="js-modal-close">{{t 'cancel'}}</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js new file mode 100644 index 00000000000..9b1c4abd26e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 ModalForm from '../../../../components/common/modal-form'; +import Template from './UpdateKeyConfirmation.hbs'; +import { parseError } from '../../../code/utils'; + +export default ModalForm.extend({ + template: Template, + + onFormSubmit () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.disableForm(); + this.showSpinner(); + + this.options.onChange(this.options.component.key, this.options.newKey) + .then(() => this.destroy()) + .catch(e => { + parseError(e).then(msg => this.showSingleError(msg)); + this.hideSpinner(); + this.enableForm(); + }); + }, + + serializeData () { + return { + component: this.options.component, + newKey: this.options.newKey + }; + } +}); + diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js index 29a627e9d54..ca20001b763 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js @@ -30,6 +30,8 @@ import { dissociateGateWithProject } from '../../../api/quality-gates'; import { getProjectLinks, createLink } from '../../../api/projectLinks'; +import { getTree } from '../../../api/components'; +import { changeKey as changeKeyApi } from '../../../api/components'; import { addGlobalSuccessMessage } from '../../../components/store/globalMessages'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -157,3 +159,29 @@ export const deleteProjectLink = (projectKey, linkId) => ({ projectKey, linkId }); + +export const RECEIVE_PROJECT_MODULES = 'RECEIVE_PROJECT_MODULES'; +const receiveProjectModules = (projectKey, modules) => ({ + type: RECEIVE_PROJECT_MODULES, + projectKey, + modules +}); + +export const fetchProjectModules = projectKey => dispatch => { + const options = { qualifiers: 'BRC', s: 'name', ps: 500 }; + getTree(projectKey, options).then(r => { + dispatch(receiveProjectModules(projectKey, r.components)); + }); +}; + +export const CHANGE_KEY = 'CHANGE_KEY'; +const changeKeyAction = (key, newKey) => ({ + type: CHANGE_KEY, + key, + newKey +}); + +export const changeKey = (key, newKey) => dispatch => { + return changeKeyApi(key, newKey) + .then(() => dispatch(changeKeyAction(key, newKey))); +}; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/components.js b/server/sonar-web/src/main/js/apps/project-admin/store/components.js new file mode 100644 index 00000000000..bc369003eee --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/components.js @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 keyBy from 'lodash/keyBy'; +import omit from 'lodash/omit'; +import { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions'; + +const components = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROJECT_MODULES) { + const newComponentsByKey = keyBy(action.modules, 'key'); + return { ...state, ...newComponentsByKey }; + } + + if (action.type === CHANGE_KEY) { + const oldComponent = state[action.key]; + if (oldComponent != null) { + const newComponent = { ...oldComponent, key: action.newKey }; + return { + ...omit(state, action.key), + [action.newKey]: newComponent + }; + } + } + + return state; +}; + +export default components; + +export const getComponentByKey = (state, key) => + state[key]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js new file mode 100644 index 00000000000..0b55882cf2d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions'; + +const modulesByProject = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROJECT_MODULES) { + const moduleKeys = action.modules.map(module => module.key); + return { ...state, [action.projectKey]: moduleKeys }; + } + + if (action.type === CHANGE_KEY) { + const newState = {}; + Object.keys(state).forEach(projectKey => { + const moduleKeys = state[projectKey]; + const changedKeyIndex = moduleKeys.indexOf(action.key); + if (changedKeyIndex !== -1) { + const newModuleKeys = [...moduleKeys]; + newModuleKeys.splice(changedKeyIndex, 1, action.newKey); + newState[projectKey] = newModuleKeys; + } else { + newState[projectKey] = moduleKeys; + } + }); + return newState; + } + + return state; +}; + +export default modulesByProject; + +export const getProjectModules = (state, projectKey) => + state[projectKey]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js index 513e39dc67d..a5c56087ce9 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js @@ -27,6 +27,12 @@ import gates, { getAllGates as nextGetAllGates, getGate } from './gates'; import gateByProject, { getProjectGate as nextGetProjectGate } from './gateByProject'; import links, { getLink } from './links'; import linksByProject, { getLinks } from './linksByProject'; +import components, { + getComponentByKey as nextGetComponentByKey +} from './components'; +import modulesByProject, { + getProjectModules as nextGetProjectModules +} from './modulesByProject'; import globalMessages, { getGlobalMessages as nextGetGlobalMessages } from '../../../components/store/globalMessages'; @@ -38,6 +44,8 @@ const rootReducer = combineReducers({ gateByProject, links, linksByProject, + components, + modulesByProject, globalMessages }); @@ -69,5 +77,16 @@ export const getProjectLinks = (state, projectKey) => getLinks(state.linksByProject, projectKey) .map(linkId => getLinkById(state, linkId)); +export const getComponentByKey = (state, componentKey) => + nextGetComponentByKey(state.components, componentKey); + +export const getProjectModules = (state, projectKey) => { + const moduleKeys = nextGetProjectModules(state.modulesByProject, projectKey); + if (moduleKeys == null) { + return null; + } + return moduleKeys.map(moduleKey => getComponentByKey(state, moduleKey)); +}; + export const getGlobalMessages = state => nextGetGlobalMessages(state.globalMessages); diff --git a/server/sonar-web/src/main/less/style.less b/server/sonar-web/src/main/less/style.less index 26ca85df14c..5743af7c82e 100644 --- a/server/sonar-web/src/main/less/style.less +++ b/server/sonar-web/src/main/less/style.less @@ -394,6 +394,7 @@ ul.bullet li { margin: 0 1px 0 0; padding: 1px 5px; .link-no-underline; + transition: none; } .tabs2 li a.selected, .tabs li a.selected, .tabs .ui-tabs-active a { diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb index d4d40889d91..a3dad4b490a 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb @@ -64,68 +64,6 @@ class ProjectController < ApplicationController def key @project = get_current_project(params[:id]) - @snapshot = @project.last_snapshot - end - - def update_key - project = get_current_project(params[:id]) - - new_key = params[:new_key].strip - if new_key.blank? - flash[:error] = message('update_key.new_key_cant_be_blank_for_x', :params => project.key) - elsif new_key == project.key - flash[:warning] = message('update_key.same_key_for_x', :params => project.key) - elsif Project.by_key(new_key) - flash[:error] = message('update_key.cant_update_x_because_resource_already_exist_with_key_x', :params => [project.key, new_key]) - else - call_backend do - Internal.component_api.updateKey(project.key, new_key) - flash[:notice] = message('update_key.key_updated') - end - end - - redirect_to :action => 'key', :id => project.root_project.id - end - - def prepare_key_bulk_update - @project = get_current_project(params[:id]) - - @string_to_replace = params[:string_to_replace].strip - @replacement_string = params[:replacement_string].strip - if @string_to_replace.blank? || @replacement_string.blank? - flash[:error] = message('update_key.fieds_cant_be_blank_for_bulk_update') - redirect_to :action => 'key', :id => @project.id - else - call_backend do - @key_check_results = Internal.component_api.checkModuleKeysBeforeRenaming(@project.key, @string_to_replace, @replacement_string) - @can_update = false - @duplicate_key_found = false - @key_check_results.each do |key, value| - if value=="#duplicate_key#" - @duplicate_key_found = true - else - @can_update = true - end - end - @can_update = false if @duplicate_key_found - end - end - end - - def perform_key_bulk_update - project = get_current_project(params[:id]) - - string_to_replace = params[:string_to_replace].strip - replacement_string = params[:replacement_string].strip - - unless string_to_replace.blank? || replacement_string.blank? - call_backend do - Internal.component_api.bulkUpdateKey(project.key, string_to_replace, replacement_string) - flash[:notice] = message('update_key.key_updated') - end - end - - redirect_to :action => 'key', :id => project.id end def history diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb deleted file mode 100644 index 471065cbd2e..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb +++ /dev/null @@ -1,18 +0,0 @@ - <tr class="<%= cycle 'even', 'odd', :name => 'modules_tree' -%>"> - <td class="thin nowrap" style="padding-left: <%= 3+ module_depth*15 -%>px"> - <%= h(current_module.key) -%> - </td> - <td class="thin nowrap"> - <% form_tag( {:action => 'update_key', :id => current_module.id }, :onsubmit => "update_launched();$j('#loading_#{id_prefix}').show();") do -%> - <input type="text" value="<%= h(current_module.key) -%>" name="new_key" id="key_<%= id_prefix -%>" size="80" maxlength="400"> - <%= submit_tag message('update_key.rename'), :id => 'update_key_' + id_prefix, :class => 'action', - :confirm => message('update_key.are_you_sure_to_rename_x', :params => current_module.key) %> - <a href="#" onclick="$j('#key_<%= id_prefix -%>').val('<%= h(current_module.key) -%>');"><%= message('update_key.reset') -%></a> - <span class="loading" id="loading_<%= id_prefix -%>" style="display: none; padding: 3px 10px;"></span> - <% end %> - </td> - </tr> - <% current_module.modules.each_with_index do |sub_module, index| %> - <%= render :partial => 'key_modules', :locals => {:current_module => sub_module, :module_depth => module_depth+1, - :id_prefix => id_prefix + (index+1).to_s} -%> - <% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb deleted file mode 100644 index 1624108b2d8..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<% - future_key = key_check_results[current_module.key] - if future_key=="#duplicate_key#" - duplicate_key = true - future_key = message('update_key.duplicate_key') - end -%> - <tr class="<%= cycle 'even', 'odd', :name => 'modules_tree' -%>"> - <td class="thin nowrap" style="padding-left: <%= 3+ module_depth*15 -%>px;"> - <%= h(current_module.key) -%> - </td> - <td class="thin nowrap <%= 'error' if duplicate_key -%>" style="<%= 'font-weight: bold;' if future_key -%>"> - <%= future_key ? future_key : current_module.key -%> - </td> - </tr> - <% current_module.modules.each_with_index do |sub_module, index| %> - <%= render :partial => 'prepare_keys', :locals => {:current_module => sub_module, :module_depth => module_depth+1, - :key_check_results => key_check_results} -%> - <% end %>
\ No newline at end of file diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb index 0ae01cfeea0..e9dd9ae3410 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb @@ -1,71 +1,3 @@ -<% - if controller.java_facade.getResourceTypeBooleanProperty(@project.qualifier, 'updatable_key') - has_modules = !@project.modules.empty? - reset_cycle 'modules_tree' -%> - -<script type="text/javascript"> - function update_launched() { - $j('input.action').each(function(index,input) { - input.disabled=true; - }); - } -</script> - -<div class="page"> - <header class="page-header"> - <h1 class="page-title"><%= message('update_key.page') -%></h1> - <p class="page-description"><%= message('update_key.page.description') -%></p> - </header> - - <% if has_modules %> - <h2><%= message('update_key.bulk_update') -%></h2> - <br/> - <p> - <%= message('update_key.bulk_change_description') -%> - <br/><br/> - <%= message('update_key.current_key_for_project_x_is_x', :params => [@project.name, @project.key]) -%> - </p> - <br/> - <% form_tag( {:action => 'prepare_key_bulk_update', :id => @project.id }, :onsubmit => "update_launched();$j('#loading_bulk_update').show();") do -%> - <table> - <tr> - <td style="padding-right: 20px"><%= message('update_key.replace') -%>:</td> - <td><input type="text" value="" name="string_to_replace" id="string_to_replace" size="40" maxlength="400"></td> - <td class="form-val-note" style="padding-left: 10px;"><%= message('update_key.replace_example') -%></td> - </tr> - <tr> - <td style="padding-right: 20px"><%= message('update_key.by') -%>:</td> - <td><input type="text" value="" name="replacement_string" id="replacement_string" size="40" maxlength="400"></td> - <td class="form-val-note" style="padding-left: 10px;"><%= message('update_key.by_example') -%></td> - </tr> - <tr> - <td></td> - <td style="padding-top: 5px"> - <%= submit_tag message('update_key.rename'), :id => 'bulk_update_button', :class => 'action' -%> - <span class="loading" id="loading_bulk_update" style="display: none; padding: 3px 10px;"></span> - </td> - <td></td> - </tr> - </table> - <% end %> - <br/> - <br/> - <h2><%= message('update_key.fine_grained_key_update') -%></h2> - <br/> - <% end %> - - <table class="data" style="width:1%"> - <thead> - <tr> - <th class="nowrap"><%= message('update_key.old_key') -%></th> - <th><%= message('update_key.new_key') -%></th> - </tr> - </thead> - <tbody> - <%= render :partial => 'key_modules', :locals => {:current_module => @project, :module_depth => 0, :id_prefix => "0"} -%> - </tbody> - </table> - - <% end %> -</div> +<% content_for :extra_script do %> + <script src="<%= ApplicationController.root_context -%>/js/bundles/project-admin.js?v=<%= sonar_version -%>"></script> +<% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb deleted file mode 100644 index 7c5bcc61074..00000000000 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb +++ /dev/null @@ -1,61 +0,0 @@ -<% - if @string_to_replace && @replacement_string - # validation screen for bulk update - reset_cycle 'modules_tree' -%> - - <script type="text/javascript"> - function update_launched() { - $j('input.action').each(function(index,input) { - input.disabled=true; - }); - } - </script> - - <div class="page"> - <header class="page-header"> - <h1 class="page-title"> - <%= @can_update ? message('update_key.bulk_update_confirmation_page') : message('update_key.bulk_update_impossible') -%> - </h1> - </header> - <p> - <% if @can_update %> - <%= message('update_key.keys_will_be_updated_as_follows') -%> - <% else %> - <% if @duplicate_key_found %> - <%= message('update_key.cant_update_because_duplicate_keys', :params => [@string_to_replace, @replacement_string]) -%> - <% else %> - <%= message('update_key.no_key_to_update', :params => @string_to_replace) -%> - <% end %> - <% end %> - </p> - - <table class="data" style="width:1%; margin-top: 10px"> - <thead> - <tr> - <th><%= message('update_key.old_key') -%></th> - <th><%= message('update_key.new_key') -%></th> - </tr> - </thead> - <tbody> - <%= render :partial => 'prepare_keys', :locals => {:current_module => @project, :module_depth => 0, :key_check_results => @key_check_results} -%> - </tbody> - </table> - - <% if @can_update %> - <% form_tag( {:action => 'perform_key_bulk_update', :id => @project.id }, :onsubmit => "update_launched();$j('#loading_bulk_update').show();") do -%> - <input type="hidden" value="<%= @project.id -%>" name="id" id="project_id"> - <input type="hidden" value="<%= @string_to_replace -%>" name="string_to_replace" id="string_to_replace"> - <input type="hidden" value="<%= @replacement_string -%>" name="replacement_string" id="replacement_string"> - <br/> - <%= submit_tag message('update_key.rename'), :id => 'bulk_update_button', :class => 'action' -%> - <a href="<%= url_for :action => 'key', :id => @project.key -%>"><%= message('cancel') -%></a> - <span class="loading" id="loading_bulk_update" style="display: none; padding: 3px 10px;"></span> - <% end %> - <% else %> - <br/> - <a href="<%= url_for :action => 'key', :id => @project.key -%>"><%= message('back') -%></a> - <% end %> - </div> - -<% end %> |