diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-07-26 16:21:19 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-07-26 16:21:19 +0200 |
commit | 940c3eb71b6c8fd6cb34c4ff93a1e661ec19a4c5 (patch) | |
tree | 195de2c237f89ada52d0d467dd9bd138581bc19c /server/sonar-web/src/main/js/apps | |
parent | fcba3fa1650b1afc794094e20764191ae8bca910 (diff) | |
download | sonarqube-940c3eb71b6c8fd6cb34c4ff93a1e661ec19a4c5.tar.gz sonarqube-940c3eb71b6c8fd6cb34c4ff93a1e661ec19a4c5.zip |
SONAR-7922 Rewrite "Quality Profiles" project page (#1118)
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
10 files changed, 555 insertions, 11 deletions
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 ee3493544d3..7bcb91f9e1c 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 @@ -19,9 +19,13 @@ */ import React from 'react'; import { render } from 'react-dom'; +import { Provider } from 'react-redux'; import { Router, Route, useRouterHistory } from 'react-router'; import { createHistory } from 'history'; import Deletion from './deletion/Deletion'; +import QualityProfiles from './quality-profiles/QualityProfiles'; +import rootReducer from './store/rootReducer'; +import configureStore from '../../components/store/configureStore'; window.sonarqube.appStarted.then(options => { const el = document.querySelector(options.el); @@ -30,12 +34,21 @@ window.sonarqube.appStarted.then(options => { basename: window.baseUrl + '/project' }); + const store = configureStore(rootReducer); + const withComponent = ComposedComponent => props => <ComposedComponent {...props} component={options.component}/>; render(( - <Router history={history}> - <Route path="/deletion" component={withComponent(Deletion)}/> - </Router> + <Provider store={store}> + <Router history={history}> + <Route + path="/deletion" + component={withComponent(Deletion)}/> + <Route + path="/quality_profiles" + component={withComponent(QualityProfiles)}/> + </Router> + </Provider> ), el); }); diff --git a/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js b/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js index 81d52fb3ba2..354935418fa 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js +++ b/server/sonar-web/src/main/js/apps/project-admin/deletion/Form.js @@ -18,24 +18,39 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import ConfirmationModal from './ConfirmationModal'; -import { translate } from '../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { deleteProject } from '../../../api/components'; export default class Form extends React.Component { static propTypes = { component: React.PropTypes.object.isRequired }; + state = { + confirmation: false, + loading: false + }; + handleDelete (e) { e.preventDefault(); - new ConfirmationModal({ project: this.props.component }) - .on('done', () => { - window.location = window.baseUrl + '/'; - }) - .render(); + this.setState({ confirmation: true }); } - render () { + confirmDeleteClick (e) { + e.preventDefault(); + this.setState({ loading: true }); + deleteProject(this.props.component.key).then(() => { + window.location = window.baseUrl + '/'; + }); + } + + cancelDeleteClick (e) { + e.preventDefault(); + e.target.blur(); + this.setState({ confirmation: false }); + } + + renderInitial () { return ( <form onSubmit={this.handleDelete.bind(this)}> <button id="delete-project" className="button-red"> @@ -44,4 +59,42 @@ export default class Form extends React.Component { </form> ); } + + renderConfirmation () { + return ( + <form className="panel panel-warning" + onSubmit={this.confirmDeleteClick.bind(this)}> + <div className="big-spacer-bottom"> + {translateWithParameters( + 'project_deletion.delete_resource_confirmation', + this.props.component.name)} + </div> + + <div> + <button + id="confirm-project-deletion" + className="button-red" + disabled={this.state.loading}> + {translate('delete')} + </button> + + {this.state.loading ? ( + <i className="spinner big-spacer-left"/> + ) : ( + <a href="#" + className="big-spacer-left" + onClick={this.cancelDeleteClick.bind(this)}> + {translate('cancel')} + </a> + )} + </div> + </form> + ); + } + + render () { + return this.state.confirmation ? + this.renderConfirmation() : + this.renderInitial(); + } } diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Header.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Header.js new file mode 100644 index 00000000000..f93fb46e926 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/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('project_quality_profiles.page')} + </h1> + <div className="page-description"> + {translate('project_quality_profiles.page.description')} + </div> + </header> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js new file mode 100644 index 00000000000..50227fa19a9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/ProfileRow.js @@ -0,0 +1,101 @@ +/* + * 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 Select from 'react-select'; +import { translate } from '../../../helpers/l10n'; + +export default class ProfileRow extends React.Component { + static propTypes = { + profile: React.PropTypes.object.isRequired, + possibleProfiles: React.PropTypes.array.isRequired, + onChangeProfile: React.PropTypes.func.isRequired + }; + + state = { + loading: false + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentWillUpdate (nextProps) { + if (nextProps.profile !== this.props.profile) { + this.setState({ loading: false }); + } + } + + handleChange (option) { + if (this.props.profile.key !== option.value) { + this.setState({ loading: true }); + this.props.onChangeProfile(this.props.profile.key, option.value); + } + } + + renderProfileName (profileOption) { + if (profileOption.isDefault) { + return ( + <span> + <strong>{translate('default')}</strong> + {': '} + {profileOption.label} + </span> + ); + } + + return profileOption.label; + } + + renderProfileSelect () { + const { profile, possibleProfiles } = this.props; + + const options = possibleProfiles.map(profile => ({ + value: profile.key, + label: profile.name, + isDefault: profile.isDefault + })); + + return ( + <Select + options={options} + valueRenderer={this.renderProfileName} + optionRenderer={this.renderProfileName} + value={profile.key} + clearable={false} + style={{ width: 300 }} + disabled={this.state.loading} + onChange={this.handleChange.bind(this)}/> + ); + } + + render () { + const { profile } = this.props; + + return ( + <tr data-key={profile.language}> + <td className="thin nowrap">{profile.languageName}</td> + <td className="thin nowrap">{this.renderProfileSelect()}</td> + <td>{this.state.loading && <i className="spinner"/>} + </td> + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js new file mode 100644 index 00000000000..766e4bf5dae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.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 { connect } from 'react-redux'; +import shallowCompare from 'react-addons-shallow-compare'; +import Header from './Header'; +import Table from './Table'; +import { fetchProjectProfiles, setProjectProfile } from '../store/actions'; +import { getProjectProfiles, getAllProfiles } from '../store/rootReducer'; + +class QualityProfiles extends React.Component { + static propTypes = { + component: React.PropTypes.object.isRequired, + allProfiles: React.PropTypes.array, + profiles: React.PropTypes.array + }; + + componentDidMount () { + this.props.fetchProjectProfiles(this.props.component.key); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleChangeProfile (oldKey, newKey) { + this.props.setProjectProfile(this.props.component.key, oldKey, newKey); + } + + render () { + const { allProfiles, profiles } = this.props; + + return ( + <div className="page page-limited"> + <Header/> + + {profiles.length > 0 ? ( + <Table + allProfiles={allProfiles} + profiles={profiles} + onChangeProfile={this.handleChangeProfile.bind(this)}/> + ) : ( + <i className="spinner"/> + )} + </div> + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + allProfiles: getAllProfiles(state), + profiles: getProjectProfiles(state, ownProps.component.key) +}); + +export default connect( + mapStateToProps, + { fetchProjectProfiles, setProjectProfile } +)(QualityProfiles); diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js new file mode 100644 index 00000000000..3e0eaae5b32 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/Table.js @@ -0,0 +1,71 @@ +/* + * 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 groupBy from 'lodash/groupBy'; +import orderBy from 'lodash/orderBy'; +import ProfileRow from './ProfileRow'; +import { translate } from '../../../helpers/l10n'; + +export default class Table extends React.Component { + static propTypes = { + allProfiles: React.PropTypes.array.isRequired, + profiles: React.PropTypes.array.isRequired, + onChangeProfile: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + renderHeader () { + // keep one empty cell for the spinner + return ( + <thead> + <tr> + <th className="thin nowrap">{translate('language')}</th> + <th className="thin nowrap">{translate('quality_profile')}</th> + <th> </th> + </tr> + </thead> + ); + } + + render () { + const profilesByLanguage = groupBy(this.props.allProfiles, 'language'); + const orderedProfiles = orderBy(this.props.profiles, 'languageName'); + + // set key to language to avoid destroying of component + const profileRows = orderedProfiles.map(profile => ( + <ProfileRow + key={profile.language} + profile={profile} + possibleProfiles={profilesByLanguage[profile.language]} + onChangeProfile={this.props.onChangeProfile}/> + )); + + return ( + <table className="data zebra"> + {this.renderHeader()} + <tbody>{profileRows}</tbody> + </table> + ); + } +} 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 new file mode 100644 index 00000000000..cad25a70e7a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js @@ -0,0 +1,70 @@ +/* + * 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 { + getQualityProfiles, + associateProject, + dissociateProject +} from '../../../api/quality-profiles'; +import { getProfileByKey } from './rootReducer'; + +export const RECEIVE_PROFILES = 'RECEIVE_PROFILES'; +export const receiveProfiles = profiles => ({ + type: RECEIVE_PROFILES, + profiles +}); + +export const RECEIVE_PROJECT_PROFILES = 'RECEIVE_PROJECT_PROFILES'; +export const receiveProjectProfiles = (projectKey, profiles) => ({ + type: RECEIVE_PROJECT_PROFILES, + projectKey, + profiles +}); + +export const fetchProjectProfiles = projectKey => dispatch => { + Promise.all([ + getQualityProfiles(), + getQualityProfiles({ projectKey }) + ]).then(responses => { + const [allProfiles, projectProfiles] = responses; + dispatch(receiveProfiles(allProfiles)); + dispatch(receiveProjectProfiles(projectKey, projectProfiles)); + }); +}; + +export const SET_PROJECT_PROFILE = 'SET_PROJECT_PROFILE'; +const setProjectProfileAction = (projectKey, oldProfileKey, newProfileKey) => ({ + type: SET_PROJECT_PROFILE, + projectKey, + oldProfileKey, + newProfileKey +}); + +export const setProjectProfile = (projectKey, oldKey, newKey) => + (dispatch, getState) => { + const state = getState(); + const newProfile = getProfileByKey(state, newKey); + const request = newProfile.isDefault ? + dissociateProject(oldKey, projectKey) : + associateProject(newKey, projectKey); + + request.then(() => { + dispatch(setProjectProfileAction(projectKey, oldKey, newKey)); + }); + }; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js b/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js new file mode 100644 index 00000000000..d3f93cc257a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/profiles.js @@ -0,0 +1,39 @@ +/* + * 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 values from 'lodash/values'; +import { RECEIVE_PROFILES } from './actions'; + +const profiles = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROFILES) { + const newProfilesByKey = keyBy(action.profiles, 'key'); + return { ...state, ...newProfilesByKey }; + } + + return state; +}; + +export default profiles; + +export const getAllProfiles = state => + values(state); + +export const getProfile = (state, key) => + state[key]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js new file mode 100644 index 00000000000..90f8be6c2cd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/profilesByProject.js @@ -0,0 +1,44 @@ +/* + * 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 without from 'lodash/without'; +import { RECEIVE_PROJECT_PROFILES, SET_PROJECT_PROFILE } from './actions'; + +const profilesByProject = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROJECT_PROFILES) { + const profileKeys = action.profiles.map(profile => profile.key); + return { ...state, [action.projectKey]: profileKeys }; + } + + if (action.type === SET_PROJECT_PROFILE) { + const profileKeys = state[action.projectKey]; + const nextProfileKeys = [ + ...without(profileKeys, action.oldProfileKey), + action.newProfileKey + ]; + return { ...state, [action.projectKey]: nextProfileKeys }; + } + + return state; +}; + +export default profilesByProject; + +export const getProfiles = (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 new file mode 100644 index 00000000000..5f5dc3d7899 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js @@ -0,0 +1,42 @@ +/* + * 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 { combineReducers } from 'redux'; +import profiles, { + getProfile, + getAllProfiles as nextGetAllProfiles +} from './profiles'; +import profilesByProject, { getProfiles } from './profilesByProject'; + +const rootReducer = combineReducers({ + profiles, + profilesByProject +}); + +export default rootReducer; + +export const getProfileByKey = (state, profileKey) => + getProfile(state.profiles, profileKey); + +export const getAllProfiles = state => + nextGetAllProfiles(state.profiles); + +export const getProjectProfiles = (state, projectKey) => + getProfiles(state.profilesByProject, projectKey) + .map(profileKey => getProfileByKey(state, profileKey)); |