From daf24bace2222f5e6f9eb6888e1e579b51e80c8e Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 12 Jan 2017 14:35:30 +0100 Subject: [PATCH] SONAR-8626 Display organization on the Projects page --- .../src/main/js/api/organizations.js | 29 ++++++++ .../src/main/js/app/components/App.js | 10 ++- .../apps/projects/components/ProjectCard.js | 27 +++++--- .../main/js/apps/projects/store/actions.js | 26 +++++++- .../main/js/components/shared/Organization.js | 61 +++++++++++++++++ .../shared/__tests__/Organization-test.js | 41 ++++++++++++ .../__snapshots__/Organization-test.js.snap | 12 ++++ .../__tests__/__snapshots__/duck-test.js.snap | 35 ++++++++++ .../organizations/__tests__/duck-test.js | 66 +++++++++++++++++++ .../src/main/js/store/organizations/duck.js | 66 +++++++++++++++++++ .../src/main/js/store/rootActions.js | 9 +++ .../src/main/js/store/rootReducer.js | 10 +++ 12 files changed, 377 insertions(+), 15 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/organizations.js create mode 100644 server/sonar-web/src/main/js/components/shared/Organization.js create mode 100644 server/sonar-web/src/main/js/components/shared/__tests__/Organization-test.js create mode 100644 server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/Organization-test.js.snap create mode 100644 server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap create mode 100644 server/sonar-web/src/main/js/store/organizations/__tests__/duck-test.js create mode 100644 server/sonar-web/src/main/js/store/organizations/duck.js diff --git a/server/sonar-web/src/main/js/api/organizations.js b/server/sonar-web/src/main/js/api/organizations.js new file mode 100644 index 00000000000..11eda583206 --- /dev/null +++ b/server/sonar-web/src/main/js/api/organizations.js @@ -0,0 +1,29 @@ +/* + * 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. + */ +// @flow +import { getJSON } from '../helpers/request'; + +export const getOrganizations = (organizations?: Array) => { + const data = {}; + if (organizations) { + Object.assign(data, { organizations: organizations.join() }); + } + return getJSON('/api/organizations/search', data); +}; diff --git a/server/sonar-web/src/main/js/app/components/App.js b/server/sonar-web/src/main/js/app/components/App.js index 359d26a0acf..cb89c3098b5 100644 --- a/server/sonar-web/src/main/js/app/components/App.js +++ b/server/sonar-web/src/main/js/app/components/App.js @@ -22,7 +22,7 @@ import React from 'react'; import { connect } from 'react-redux'; import GlobalLoading from './GlobalLoading'; import { fetchCurrentUser } from '../../store/users/actions'; -import { fetchLanguages, fetchAppState } from '../../store/rootActions'; +import { fetchLanguages, fetchAppState, fetchOrganizations } from '../../store/rootActions'; class App extends React.Component { mounted: boolean; @@ -31,6 +31,7 @@ class App extends React.Component { fetchAppState: React.PropTypes.func.isRequired, fetchCurrentUser: React.PropTypes.func.isRequired, fetchLanguages: React.PropTypes.func.isRequired, + fetchOrganizations: React.PropTypes.func.isRequired, children: React.PropTypes.element.isRequired }; @@ -48,7 +49,10 @@ class App extends React.Component { this.mounted = true; this.props.fetchCurrentUser() - .then(this.props.fetchAppState) + .then(() => Promise.all([ + this.props.fetchAppState(), + this.props.fetchOrganizations() + ])) .then(this.finishLoading) .then(this.props.fetchLanguages) .catch(this.finishLoading); @@ -69,5 +73,5 @@ class App extends React.Component { export default connect( null, - { fetchAppState, fetchCurrentUser, fetchLanguages } + { fetchAppState, fetchCurrentUser, fetchLanguages, fetchOrganizations } )(App); 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 fe151b6caae..8217c7f0fa9 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 @@ -24,6 +24,7 @@ import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardMeasures from './ProjectCardMeasures'; import FavoriteContainer from '../../../components/controls/FavoriteContainer'; import { translate } from '../../../helpers/l10n'; +import Organization from '../../../components/shared/Organization'; export default class ProjectCard extends React.Component { static propTypes = { @@ -43,6 +44,7 @@ export default class ProjectCard extends React.Component { const className = classNames('boxed-group', 'project-card', { 'boxed-group-loading': !areProjectMeasuresLoaded }); + return (
{isProjectAnalyzed && ( @@ -55,22 +57,27 @@ export default class ProjectCard extends React.Component { )}

+ {project.organization != null && ( + + + + )} {project.name}

{isProjectAnalyzed ? ( -
- -
- ) : ( -
-
- {translate('projects.not_analyzed')} -
-
- )} +
+ +
+ ) : ( +
+
+ {translate('projects.not_analyzed')} +
+
+ )} ); } 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 7e3e0f0800d..91edf726165 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 @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import groupBy from 'lodash/groupBy'; +import uniq from 'lodash/uniq'; import { searchProjects } from '../../../api/components'; import { addGlobalErrorMessage } from '../../../store/globalMessages/duck'; import { parseError } from '../../code/utils'; @@ -29,6 +30,8 @@ import { getMeasuresForProjects } from '../../../api/measures'; import { receiveComponentsMeasures } from '../../../store/measures/actions'; import { convertToFilter } from './utils'; import { receiveFavorites } from '../../../store/favorites/duck'; +import { getOrganizations } from '../../../api/organizations'; +import { receiveOrganizations } from '../../../store/organizations/duck'; const PAGE_SIZE = 50; @@ -78,6 +81,10 @@ const onReceiveMeasures = (dispatch, expectedProjectKeys) => response => { dispatch(receiveComponentsMeasures(toStore)); }; +const onReceiveOrganizations = dispatch => response => { + dispatch(receiveOrganizations(response.organizations)); +}; + const fetchProjectMeasures = projects => dispatch => { if (!projects.length) { return Promise.resolve(); @@ -87,6 +94,15 @@ const fetchProjectMeasures = projects => dispatch => { return getMeasuresForProjects(projectKeys, METRICS).then(onReceiveMeasures(dispatch, projectKeys), onFail(dispatch)); }; +const fetchProjectOrganizations = projects => dispatch => { + if (!projects.length) { + return Promise.resolve(); + } + + const organizationKeys = uniq(projects.map(project => project.organization)); + return getOrganizations(organizationKeys).then(onReceiveOrganizations(dispatch), onFail(dispatch)); +}; + const handleFavorites = (dispatch, projects) => { const toAdd = projects.filter(project => project.isFavorite); const toRemove = projects.filter(project => project.isFavorite === false); @@ -99,7 +115,10 @@ const onReceiveProjects = dispatch => response => { dispatch(receiveComponents(response.components)); dispatch(receiveProjects(response.components, response.facets)); handleFavorites(dispatch, response.components); - dispatch(fetchProjectMeasures(response.components)).then(() => { + Promise.all([ + dispatch(fetchProjectMeasures(response.components)), + dispatch(fetchProjectOrganizations(response.components)) + ]).then(() => { dispatch(updateState({ loading: false })); }); dispatch(updateState({ @@ -112,7 +131,10 @@ const onReceiveMoreProjects = dispatch => response => { dispatch(receiveComponents(response.components)); dispatch(receiveMoreProjects(response.components)); handleFavorites(dispatch, response.components); - dispatch(fetchProjectMeasures(response.components)).then(() => { + Promise.all([ + dispatch(fetchProjectMeasures(response.components)), + dispatch(fetchProjectOrganizations(response.components)) + ]).then(() => { dispatch(updateState({ loading: false })); }); dispatch(updateState({ pageIndex: response.paging.pageIndex })); diff --git a/server/sonar-web/src/main/js/components/shared/Organization.js b/server/sonar-web/src/main/js/components/shared/Organization.js new file mode 100644 index 00000000000..716ab38023d --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/Organization.js @@ -0,0 +1,61 @@ +/* + * 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. + */ +// @flow +import React from 'react'; +import { connect } from 'react-redux'; +import { getOrganizationByKey, areThereCustomOrganizations } from '../../store/rootReducer'; + +type OwnProps = { + organizationKey: string, +}; + +type Props = { + organizationKey: string, + organization: null | { + key: string, + name: string + }, + shouldBeDisplayed: boolean +}; + +class Organization extends React.Component { + props: Props; + + render () { + const { organization: org, shouldBeDisplayed } = this.props; + + if (!shouldBeDisplayed || !org) { + return null; + } + + return ( + {org.name} /  + ); + } +} + +const mapStateToProps = (state, ownProps: OwnProps) => ({ + organization: getOrganizationByKey(state, ownProps.organizationKey), + shouldBeDisplayed: areThereCustomOrganizations(state) +}); + +export default connect(mapStateToProps)(Organization); + +export const UnconnectedOrganization = Organization; diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/Organization-test.js b/server/sonar-web/src/main/js/components/shared/__tests__/Organization-test.js new file mode 100644 index 00000000000..bc08404390f --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/__tests__/Organization-test.js @@ -0,0 +1,41 @@ +/* + * 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 { shallow } from 'enzyme'; +import { UnconnectedOrganization } from '../Organization'; + +const organization = { key: 'foo', name: 'foo' }; + +it('should match snapshot', () => { + expect(shallow( + + )).toMatchSnapshot(); +}); + +it('should not be displayed', () => { + expect(shallow( + + )).toMatchSnapshot(); + + expect(shallow( + + )).toMatchSnapshot(); +}); + diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/Organization-test.js.snap b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/Organization-test.js.snap new file mode 100644 index 00000000000..698486d6465 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/Organization-test.js.snap @@ -0,0 +1,12 @@ +exports[`test should match snapshot 1`] = ` + + foo + +  /  + + +`; + +exports[`test should not be displayed 1`] = `null`; + +exports[`test should not be displayed 2`] = `null`; diff --git a/server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap b/server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap new file mode 100644 index 00000000000..158cccb9dde --- /dev/null +++ b/server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap @@ -0,0 +1,35 @@ +exports[`Reducer should have initial state 1`] = ` +Object { + "byKey": Object {}, +} +`; + +exports[`Reducer should receive organizations 1`] = ` +Object { + "byKey": Object { + "bar": Object { + "key": "bar", + "name": "Bar", + }, + "foo": Object { + "key": "foo", + "name": "Foo", + }, + }, +} +`; + +exports[`Reducer should receive organizations 2`] = ` +Object { + "byKey": Object { + "bar": Object { + "key": "bar", + "name": "Bar", + }, + "foo": Object { + "key": "foo", + "name": "Qwe", + }, + }, +} +`; diff --git a/server/sonar-web/src/main/js/store/organizations/__tests__/duck-test.js b/server/sonar-web/src/main/js/store/organizations/__tests__/duck-test.js new file mode 100644 index 00000000000..711609319a5 --- /dev/null +++ b/server/sonar-web/src/main/js/store/organizations/__tests__/duck-test.js @@ -0,0 +1,66 @@ +/* + * 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 organizations, { getOrganizationByKey, areThereCustomOrganizations } from '../duck'; + +describe('Reducer', () => { + it('should have initial state', () => { + expect(organizations(undefined, {})).toMatchSnapshot(); + }); + + it('should receive organizations', () => { + const state0 = { byKey: {} }; + + const action1 = { + type: 'RECEIVE_ORGANIZATIONS', + organizations: [ + { key: 'foo', name: 'Foo' }, + { key: 'bar', name: 'Bar' } + ] + }; + const state1 = organizations(state0, action1); + expect(state1).toMatchSnapshot(); + + const action2 = { + type: 'RECEIVE_ORGANIZATIONS', + organizations: [ + { key: 'foo', name: 'Qwe' } + ] + }; + const state2 = organizations(state1, action2); + expect(state2).toMatchSnapshot(); + }); +}); + +describe('Selectors', () => { + it('getOrganizationByKey', () => { + const foo = { key: 'foo', name: 'Foo' }; + const state = { byKey: { foo } }; + expect(getOrganizationByKey(state, 'foo')).toBe(foo); + expect(getOrganizationByKey(state, 'bar')).toBeFalsy(); + }); + + it('areThereCustomOrganizations', () => { + const foo = { key: 'foo', name: 'Foo' }; + const bar = { key: 'bar', name: 'Bar' }; + expect(areThereCustomOrganizations({ byKey: {} }, 'foo')).toBe(false); + expect(areThereCustomOrganizations({ byKey: { foo } }, 'foo')).toBe(false); + expect(areThereCustomOrganizations({ byKey: { foo, bar } }, 'foo')).toBe(true); + }); +}); diff --git a/server/sonar-web/src/main/js/store/organizations/duck.js b/server/sonar-web/src/main/js/store/organizations/duck.js new file mode 100644 index 00000000000..8061b06b6bb --- /dev/null +++ b/server/sonar-web/src/main/js/store/organizations/duck.js @@ -0,0 +1,66 @@ +/* + * 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. + */ +// @flow +import { combineReducers } from 'redux'; +import keyBy from 'lodash/keyBy'; + +export type Organization = { + key: string, + name: string +}; + +type ReceiveOrganizationsAction = { + type: 'RECEIVE_ORGANIZATIONS', + organizations: Array +}; + +type Action = ReceiveOrganizationsAction; + +type ByKeyState = { + [key: string]: Organization +}; + +type State = { + byKey: ByKeyState +}; + +export const receiveOrganizations = (organizations: Array): ReceiveOrganizationsAction => ({ + type: 'RECEIVE_ORGANIZATIONS', + organizations +}); + +const byKey = (state: ByKeyState = {}, action: Action) => { + switch (action.type) { + case 'RECEIVE_ORGANIZATIONS': + return { ...state, ...keyBy(action.organizations, 'key') }; + default: + return state; + } +}; + +export default combineReducers({ byKey }); + +export const getOrganizationByKey = (state: State, key: string): Organization => ( + state.byKey[key] +); + +export const areThereCustomOrganizations = (state: State): boolean => ( + Object.keys(state.byKey).length > 1 +); diff --git a/server/sonar-web/src/main/js/store/rootActions.js b/server/sonar-web/src/main/js/store/rootActions.js index c6408588568..00ff3971185 100644 --- a/server/sonar-web/src/main/js/store/rootActions.js +++ b/server/sonar-web/src/main/js/store/rootActions.js @@ -20,11 +20,13 @@ import { getLanguages } from '../api/languages'; import { getGlobalNavigation, getComponentNavigation } from '../api/nav'; import * as auth from '../api/auth'; +import { getOrganizations } from '../api/organizations'; import { receiveLanguages } from './languages/actions'; import { receiveComponents } from './components/actions'; import { addGlobalErrorMessage } from './globalMessages/duck'; import { parseError } from '../apps/code/utils'; import { setAppState } from './appState/duck'; +import { receiveOrganizations } from './organizations/duck'; export const onFail = dispatch => error => ( parseError(error).then(message => dispatch(addGlobalErrorMessage(message))) @@ -44,6 +46,13 @@ export const fetchLanguages = () => dispatch => { ); }; +export const fetchOrganizations = () => dispatch => ( + getOrganizations().then( + r => dispatch(receiveOrganizations(r.organizations)), + onFail(dispatch) + ) +); + const addQualifier = project => ({ ...project, qualifier: project.breadcrumbs[project.breadcrumbs.length - 1].qualifier diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 05174e1f9be..e3a26024c09 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -25,6 +25,7 @@ import favorites, * as fromFavorites from './favorites/duck'; import languages, * as fromLanguages from './languages/reducer'; import measures, * as fromMeasures from './measures/reducer'; import notifications, * as fromNotifications from './notifications/duck'; +import organizations, * as fromOrganizations from './organizations/duck'; import globalMessages, * as fromGlobalMessages from './globalMessages/duck'; import projectActivity from './projectActivity/duck'; import measuresApp, * as fromMeasuresApp from '../apps/component-measures/store/rootReducer'; @@ -42,6 +43,7 @@ export default combineReducers({ languages, measures, notifications, + organizations, projectActivity, users, @@ -110,6 +112,14 @@ export const getNotificationPerProjectTypes = state => ( fromNotifications.getPerProjectTypes(state.notifications) ); +export const getOrganizationByKey = (state, key) => ( + fromOrganizations.getOrganizationByKey(state.organizations, key) +); + +export const areThereCustomOrganizations = state => ( + fromOrganizations.areThereCustomOrganizations(state.organizations) +); + export const getProjectActivity = state => ( state.projectActivity ); -- 2.39.5