diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-11-09 09:41:59 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-11-09 15:51:40 +0100 |
commit | f662a342d352376b9cc6854e20c58b205f184e45 (patch) | |
tree | e19b88ef04079f40044ec990c0bc8d7d37d5db87 /server/sonar-web | |
parent | 26f5f414b64fd9d3963c859da7c8f14616ddb759 (diff) | |
download | sonarqube-f662a342d352376b9cc6854e20c58b205f184e45.tar.gz sonarqube-f662a342d352376b9cc6854e20c58b205f184e45.zip |
SONAR-8362 Display information of favorite projects
Diffstat (limited to 'server/sonar-web')
13 files changed, 242 insertions, 107 deletions
diff --git a/server/sonar-web/src/main/js/app/store/favorites/actions.js b/server/sonar-web/src/main/js/app/store/favorites/actions.js deleted file mode 100644 index 2fc7db49085..00000000000 --- a/server/sonar-web/src/main/js/app/store/favorites/actions.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ -export const RECEIVE_FAVORITES = 'RECEIVE_FAVORITES'; - -export const receiveFavorites = favorites => ({ - type: RECEIVE_FAVORITES, - favorites -}); diff --git a/server/sonar-web/src/main/js/app/store/favorites/duck.js b/server/sonar-web/src/main/js/app/store/favorites/duck.js new file mode 100644 index 00000000000..7c6d4b52012 --- /dev/null +++ b/server/sonar-web/src/main/js/app/store/favorites/duck.js @@ -0,0 +1,63 @@ +/* + * 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 uniq from 'lodash/uniq'; +import without from 'lodash/without'; + +export const actions = { + RECEIVE_FAVORITES: 'RECEIVE_FAVORITES', + ADD_FAVORITE: 'ADD_FAVORITE', + REMOVE_FAVORITE: 'REMOVE_FAVORITE' +}; + +export const receiveFavorites = favorites => ({ + type: actions.RECEIVE_FAVORITES, + favorites +}); + +export const addFavorite = componentKey => ({ + type: actions.ADD_FAVORITE, + componentKey +}); + +export const removeFavorite = componentKey => ({ + type: actions.REMOVE_FAVORITE, + componentKey +}); + +export default (state = [], action = {}) => { + if (action.type === actions.RECEIVE_FAVORITES) { + return uniq([...state, ...action.favorites.map(f => f.key)]); + } + + if (action.type === actions.ADD_FAVORITE) { + return uniq([...state, action.componentKey]); + } + + if (action.type === actions.REMOVE_FAVORITE) { + return without(state, action.componentKey); + } + + return state; +}; + +export const isFavorite = (state, componentKey) => ( + state.includes(componentKey) +); + diff --git a/server/sonar-web/src/main/js/app/store/favorites/reducer.js b/server/sonar-web/src/main/js/app/store/favorites/reducer.js deleted file mode 100644 index 29d19fece37..00000000000 --- a/server/sonar-web/src/main/js/app/store/favorites/reducer.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 keyBy from 'lodash/keyBy'; -import { RECEIVE_FAVORITES } from './actions'; - -const favoritesByKey = (state = {}, action = {}) => { - if (action.type === RECEIVE_FAVORITES) { - const byKey = keyBy(action.favorites, 'key'); - return { ...state, ...byKey }; - } - - return state; -}; - -const favoriteKeys = (state = null, action = {}) => { - if (action.type === RECEIVE_FAVORITES) { - return action.favorites.map(f => f.key); - } - - return state; -}; - -export default combineReducers({ favoritesByKey, favoriteKeys }); - -export const getFavorites = state => ( - state.favoriteKeys ? - state.favoriteKeys.map(key => state.favoritesByKey[key]) : - null -); diff --git a/server/sonar-web/src/main/js/app/store/rootReducer.js b/server/sonar-web/src/main/js/app/store/rootReducer.js index 3c2ac6abb2b..40c02dafbf7 100644 --- a/server/sonar-web/src/main/js/app/store/rootReducer.js +++ b/server/sonar-web/src/main/js/app/store/rootReducer.js @@ -20,7 +20,7 @@ import { combineReducers } from 'redux'; import components, * as fromComponents from './components/reducer'; import users, * as fromUsers from './users/reducer'; -import favorites, * as fromFavorites from './favorites/reducer'; +import favorites, * as fromFavorites from './favorites/duck'; import languages, * as fromLanguages from './languages/reducer'; import measures, * as fromMeasures from './measures/reducer'; import globalMessages, * as fromGlobalMessages from '../../components/store/globalMessages'; @@ -57,8 +57,8 @@ export const getCurrentUser = state => ( fromUsers.getCurrentUser(state.users) ); -export const getFavorites = state => ( - fromFavorites.getFavorites(state.favorites) +export const isFavorite = (state, componentKey) => ( + fromFavorites.isFavorite(state.favorites, componentKey) ); export const getComponentMeasure = (state, componentKey, metricKey) => ( diff --git a/server/sonar-web/src/main/js/app/styles/boxed-group.css b/server/sonar-web/src/main/js/app/styles/boxed-group.css index 6709d652381..3b3836cb6cc 100644 --- a/server/sonar-web/src/main/js/app/styles/boxed-group.css +++ b/server/sonar-web/src/main/js/app/styles/boxed-group.css @@ -16,6 +16,26 @@ margin: 15px -20px; } +.boxed-group-header { + padding: 15px 20px 0; +} + +.boxed-group-header > h2 { + display: inline-block; + vertical-align: middle; + line-height: 24px; +} + +.boxed-group-header > [class^="icon-"] { + display: inline-block; + vertical-align: middle; +} + +.boxed-group-header > .icon-star { + position: relative; + top: 1px; +} + .boxed-group-actions { float: right; margin-top: 15px; diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js index 9b9ec7fc11f..79a4b28d9f6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js @@ -21,6 +21,7 @@ import React from 'react'; import classNames from 'classnames'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardMeasures from './ProjectCardMeasures'; +import FavoriteContainer from '../../../components/controls/FavoriteContainer'; import { getComponentUrl } from '../../../helpers/urls'; export default class ProjectCard extends React.Component { @@ -44,9 +45,14 @@ export default class ProjectCard extends React.Component { <ProjectCardQualityGate status={this.props.measures['alert_status']}/> </div> )} - <h2 className="project-card-name"> - <a className="link-base-color" href={getComponentUrl(project.key)}>{project.name}</a> - </h2> + <div className="boxed-group-header"> + {project.isFavorite != null && ( + <FavoriteContainer className="spacer-right" componentKey={project.key}/> + )} + <h2 className="project-card-name"> + <a className="link-base-color" href={getComponentUrl(project.key)}>{project.name}</a> + </h2> + </div> <div className="boxed-group-inner"> <ProjectCardMeasures measures={this.props.measures}/> </div> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js index 1f60437b534..0b81e2ceb8a 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js @@ -34,7 +34,7 @@ export default class ProjectCardQualityGate extends React.Component { } return ( - <div className="project-card-measure"> + <div className="project-card-measure project-card-quality-gate"> <div className="project-card-measure-inner"> <span className="small spacer-right"> {translate('overview.quality_gate')} diff --git a/server/sonar-web/src/main/js/apps/projects/store/actions.js b/server/sonar-web/src/main/js/apps/projects/store/actions.js index 9582b7a7dee..922400ead59 100644 --- a/server/sonar-web/src/main/js/apps/projects/store/actions.js +++ b/server/sonar-web/src/main/js/apps/projects/store/actions.js @@ -28,8 +28,7 @@ import { getProjectsAppState } from '../../../app/store/rootReducer'; import { getMeasuresForProjects } from '../../../api/measures'; import { receiveComponentsMeasures } from '../../../app/store/measures/actions'; import { convertToFilter } from './utils'; -import { getFavorites } from '../../../api/favorites'; -import { receiveFavorites } from '../../../app/store/favorites/actions'; +import { receiveFavorites } from '../../../app/store/favorites/duck'; const PAGE_SIZE = 50; @@ -84,9 +83,17 @@ const fetchProjectMeasures = projects => dispatch => { return getMeasuresForProjects(projectKeys, METRICS).then(onReceiveMeasures(dispatch), onFail(dispatch)); }; +const handleFavorites = (dispatch, projects) => { + const favorites = projects.filter(project => project.isFavorite); + if (favorites.length) { + dispatch(receiveFavorites(favorites)); + } +}; + const onReceiveProjects = dispatch => response => { dispatch(receiveComponents(response.components)); dispatch(receiveProjects(response.components, response.facets)); + handleFavorites(dispatch, response.components); dispatch(fetchProjectMeasures(response.components)).then(() => { dispatch(updateState({ loading: false })); }); @@ -99,6 +106,7 @@ const onReceiveProjects = dispatch => response => { const onReceiveMoreProjects = dispatch => response => { dispatch(receiveComponents(response.components)); dispatch(receiveMoreProjects(response.components)); + handleFavorites(dispatch, response.components); dispatch(fetchProjectMeasures(response.components)).then(() => { dispatch(updateState({ loading: false })); }); @@ -126,23 +134,3 @@ export const fetchMoreProjects = query => (dispatch, getState) => { } return searchProjects(data).then(onReceiveMoreProjects(dispatch), onFail(dispatch)); }; - -export const fetchFavoriteProjects = () => dispatch => { - dispatch(updateState({ loading: true })); - - return getFavorites().then(favorites => { - dispatch(receiveFavorites(favorites)); - - const projects = favorites.filter(component => component.qualifier === 'TRK'); - - dispatch(receiveComponents(projects)); - dispatch(receiveProjects(projects, [])); - dispatch(fetchProjectMeasures(projects)).then(() => { - dispatch(updateState({ loading: false })); - }); - dispatch(updateState({ - total: projects.length, - pageIndex: 1 - })); - }, onFail(dispatch)); -}; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index a71aceb70d2..d9ac7cbd9c5 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -80,6 +80,10 @@ font-size: 12px; } +.project-card-quality-gate { + line-height: 24px; +} + .projects-facet-header { padding-top: 10px; padding-bottom: 10px; diff --git a/server/sonar-web/src/main/js/components/controls/Favorite.js b/server/sonar-web/src/main/js/components/controls/Favorite.js index 5e35ba2f1b2..92767a8655b 100644 --- a/server/sonar-web/src/main/js/components/controls/Favorite.js +++ b/server/sonar-web/src/main/js/components/controls/Favorite.js @@ -24,15 +24,19 @@ import { addFavorite, removeFavorite } from '../../api/favorites'; export default class Favorite extends React.Component { static propTypes = { favorite: React.PropTypes.bool.isRequired, - component: React.PropTypes.string.isRequired + component: React.PropTypes.string.isRequired, + className: React.PropTypes.string }; render () { + const { favorite, component, ...other } = this.props; + return ( <FavoriteBase - favorite={this.props.favorite} - addFavorite={() => addFavorite(this.props.component)} - removeFavorite={() => removeFavorite(this.props.component)}/> + {...other} + favorite={favorite} + addFavorite={() => addFavorite(component)} + removeFavorite={() => removeFavorite(component)}/> ); } } diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBase.js b/server/sonar-web/src/main/js/components/controls/FavoriteBase.js index 04ee1fc4640..8749b2ebd77 100644 --- a/server/sonar-web/src/main/js/components/controls/FavoriteBase.js +++ b/server/sonar-web/src/main/js/components/controls/FavoriteBase.js @@ -24,7 +24,8 @@ export default class FavoriteBase extends React.Component { static propTypes = { favorite: React.PropTypes.bool.isRequired, addFavorite: React.PropTypes.func.isRequired, - removeFavorite: React.PropTypes.func.isRequired + removeFavorite: React.PropTypes.func.isRequired, + className: React.PropTypes.string }; constructor (props) { @@ -79,7 +80,7 @@ export default class FavoriteBase extends React.Component { render () { const className = classNames('icon-star', { 'icon-star-favorite': this.state.favorite - }); + }, this.props.className); return ( <a className={className} diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBaseStateless.js b/server/sonar-web/src/main/js/components/controls/FavoriteBaseStateless.js new file mode 100644 index 00000000000..d648e4fff95 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/FavoriteBaseStateless.js @@ -0,0 +1,63 @@ +/* + * 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 classNames from 'classnames'; + +export default class FavoriteBaseStateless extends React.Component { + static propTypes = { + favorite: React.PropTypes.bool.isRequired, + addFavorite: React.PropTypes.func.isRequired, + removeFavorite: React.PropTypes.func.isRequired, + className: React.PropTypes.string + }; + + toggleFavorite = e => { + e.preventDefault(); + if (this.props.favorite) { + this.props.removeFavorite(); + } else { + this.props.addFavorite(); + } + }; + + renderSVG () { + /* eslint max-len: 0 */ + return ( + <svg width="16" height="16"> + <path + d="M15.4275,5.77678C15.4275,5.90773 15.3501,6.05059 15.1953,6.20536L11.9542,9.36608L12.7221,13.8304C12.728,13.872 12.731,13.9316 12.731,14.0089C12.731,14.1339 12.6998,14.2396 12.6373,14.3259C12.5748,14.4122 12.484,14.4554 12.3649,14.4554C12.2518,14.4554 12.1328,14.4197 12.0078,14.3482L7.99888,12.2411L3.98995,14.3482C3.85901,14.4197 3.73996,14.4554 3.63281,14.4554C3.50781,14.4554 3.41406,14.4122 3.35156,14.3259C3.28906,14.2396 3.25781,14.1339 3.25781,14.0089C3.25781,13.9732 3.26377,13.9137 3.27567,13.8304L4.04353,9.36608L0.793531,6.20536C0.644719,6.04464 0.570313,5.90178 0.570313,5.77678C0.570313,5.55654 0.736979,5.41964 1.07031,5.36606L5.55245,4.71428L7.56138,0.651781C7.67447,0.407729 7.8203,0.285703 7.99888,0.285703C8.17745,0.285703 8.32328,0.407729 8.43638,0.651781L10.4453,4.71428L14.9274,5.36606C15.2608,5.41964 15.4274,5.55654 15.4274,5.77678L15.4275,5.77678Z"/> + </svg> + ); + } + + render () { + const className = classNames('icon-star', { + 'icon-star-favorite': this.props.favorite + }, this.props.className); + + return ( + <a className={className} + href="#" + onClick={this.toggleFavorite}> + {this.renderSVG()} + </a> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteContainer.js b/server/sonar-web/src/main/js/components/controls/FavoriteContainer.js new file mode 100644 index 00000000000..b2157ced05f --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/FavoriteContainer.js @@ -0,0 +1,58 @@ +/* + * 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 { connect } from 'react-redux'; +import FavoriteBaseStateless from './FavoriteBaseStateless'; +import { isFavorite } from '../../app/store/rootReducer'; +import * as actionCreators from '../../app/store/favorites/duck'; +import * as api from '../../api/favorites'; +import { addGlobalErrorMessage } from '../store/globalMessages'; +import { parseError } from '../../apps/code/utils'; + +const addFavorite = componentKey => dispatch => { + // optimistic update + dispatch(actionCreators.addFavorite(componentKey)); + api.addFavorite(componentKey).catch(error => { + dispatch(actionCreators.removeFavorite(componentKey)); + parseError(error).then(message => dispatch(addGlobalErrorMessage(message))); + }); +}; + +const removeFavorite = componentKey => dispatch => { + // optimistic update + dispatch(actionCreators.removeFavorite(componentKey)); + api.removeFavorite(componentKey).catch(error => { + dispatch(actionCreators.addFavorite(componentKey)); + parseError(error).then(message => dispatch(addGlobalErrorMessage(message))); + }); +}; + +const mapStateToProps = (state, ownProps) => ({ + favorite: isFavorite(state, ownProps.componentKey) +}); + +const mapDispatchToProps = (dispatch, ownProps) => ({ + addFavorite: () => dispatch(addFavorite(ownProps.componentKey)), + removeFavorite: () => dispatch(removeFavorite(ownProps.componentKey)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(FavoriteBaseStateless); |