From 62d5677706d8c96750c13dc21aa4a4f314cbe47f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 2 Oct 2018 08:52:56 +0200 Subject: [PATCH] SONAR-11289 Make portfolio creation popup accessible to non global admin --- .../embed-docs-modal/EmbedDocsPopup.tsx | 22 --- .../embed-docs-modal/EmbedDocsPopupHelper.tsx | 9 +- .../__tests__/EmbedDocsPopup-test.tsx | 48 +---- .../nav/component/ComponentNavBranch.tsx | 9 +- .../app/components/nav/global/GlobalNav.tsx | 17 +- .../components/nav/global/GlobalNavPlus.tsx | 175 +++++++++++++++--- .../global/__tests__/GlobalNavPlus-test.tsx | 81 +++++++- .../__snapshots__/GlobalNav-test.tsx.snap | 12 -- .../__snapshots__/GlobalNavPlus-test.tsx.snap | 71 +++++-- .../portfolio/components/CreateFormShim.tsx | 33 ++++ server/sonar-web/src/main/js/helpers/urls.ts | 4 + .../resources/org/sonar/l10n/core.properties | 4 + 12 files changed, 335 insertions(+), 150 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx index c62d51dfd5e..5cc79d2d179 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx @@ -18,33 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; import ProductNewsMenuItem from './ProductNewsMenuItem'; import { SuggestionLink } from './SuggestionsProvider'; -import { CurrentUser, isLoggedIn, hasGlobalPermission } from '../../types'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/urls'; import { isSonarCloud } from '../../../helpers/system'; import { DropdownOverlay } from '../../../components/controls/Dropdown'; interface Props { - currentUser: CurrentUser; onClose: () => void; suggestions: Array; } export default class EmbedDocsPopup extends React.PureComponent { - static contextTypes = { - openProjectOnboarding: PropTypes.func - }; - - onAnalyzeProjectClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); - this.context.openProjectOnboarding(); - }; - renderTitle(text: string) { return
  • {text}
  • ; } @@ -119,17 +106,8 @@ export default class EmbedDocsPopup extends React.PureComponent { } renderSonarQubeLinks() { - const { currentUser } = this.props; return ( - {isLoggedIn(currentUser) && - hasGlobalPermission(currentUser, 'provisioning') && ( -
  • - - {translate('embed_docs.analyze_new_project')} - -
  • - )}
  • diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx index ff39f16ad71..8e2486b5025 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx @@ -19,7 +19,6 @@ */ import * as React from 'react'; import { SuggestionLink } from './SuggestionsProvider'; -import { CurrentUser } from '../../types'; import Toggler from '../../../components/controls/Toggler'; import HelpIcon from '../../../components/icons-components/HelpIcon'; import { lazyLoad } from '../../../components/lazyLoad'; @@ -28,9 +27,7 @@ import { translate } from '../../../helpers/l10n'; const EmbedDocsPopup = lazyLoad(() => import('./EmbedDocsPopup')); interface Props { - currentUser: CurrentUser; suggestions: Array; - tooltip: boolean; } interface State { helpOpen: boolean; @@ -85,11 +82,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent + }> diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx index 24f5fbec4d6..e21c57fb020 100644 --- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx +++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx @@ -28,55 +28,19 @@ const suggestions = [{ link: '#', text: 'foo' }, { link: '#', text: 'bar' }]; it('should display suggestion links', () => { const context = {}; - const wrapper = shallow( - , - { - context - } - ); + const wrapper = shallow(, { + context + }); wrapper.update(); expect(wrapper).toMatchSnapshot(); }); -it('should display analyze new project link when user has permission', () => { - const wrapper = shallow( - - ); - expect(wrapper.find('[data-test="analyze-new-project"]').exists()).toBe(true); -}); - -it('should not display analyze new project link when user does not have permission', () => { - const wrapper = shallow( - - ); - expect(wrapper.find('[data-test="analyze-new-project"]').exists()).toBe(false); -}); - it('should display correct links for SonarCloud', () => { (isSonarCloud as jest.Mock).mockReturnValueOnce(true); const context = {}; - const wrapper = shallow( - , - { - context - } - ); + const wrapper = shallow(, { + context + }); wrapper.update(); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 069472c96e5..fbdad095b35 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -38,6 +38,7 @@ import HelpTooltip from '../../../../components/controls/HelpTooltip'; import Toggler from '../../../../components/controls/Toggler'; import DropdownIcon from '../../../../components/icons-components/DropdownIcon'; import { isSonarCloud } from '../../../../helpers/system'; +import { getPortfolioAdminUrl } from '../../../../helpers/urls'; interface Props { branchLikes: BranchLike[]; @@ -128,15 +129,13 @@ export default class ComponentNavBranch extends React.PureComponent { - const adminLink = { - pathname: '/project/admin/extension/governance/console', - query: { id: this.props.component.breadcrumbs[0].key, qualifier: 'APP' } - }; return ( <>

    {translate('application.branches.help')}


    - + {translate('application.branches.link')} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index 396050e4731..2c1aa0f6813 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -61,16 +61,15 @@ export class GlobalNav extends React.PureComponent {
      {isSonarCloud() && } - + - {isLoggedIn(this.props.currentUser) && - isSonarCloud() && ( - - )} + {isLoggedIn(this.props.currentUser) && ( + + )}
    diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx index 57f21824533..e702ca72b3d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -18,47 +18,166 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; -import PlusIcon from '../../../../components/icons-components/PlusIcon'; +import { Link, withRouter, WithRouterProps } from 'react-router'; +import CreateFormShim from '../../../../apps/portfolio/components/CreateFormShim'; import Dropdown from '../../../../components/controls/Dropdown'; +import PlusIcon from '../../../../components/icons-components/PlusIcon'; +import { AppState, hasGlobalPermission, CurrentUser } from '../../../types'; +import { getPortfolioAdminUrl } from '../../../../helpers/urls'; +import { getExtensionStart } from '../../extensions/utils'; +import { isSonarCloud } from '../../../../helpers/system'; import { translate } from '../../../../helpers/l10n'; interface Props { + appState: Pick; + currentUser: CurrentUser; openProjectOnboarding: () => void; } -export default class GlobalNavPlus extends React.PureComponent { - handleNewProjectClick = (event: React.SyntheticEvent) => { +interface State { + createPortfolio: boolean; + governanceReady: boolean; +} + +export class GlobalNavPlus extends React.PureComponent { + mounted = false; + state: State = { createPortfolio: false, governanceReady: false }; + + componentDidMount() { + this.mounted = true; + if (this.props.appState.qualifiers.includes('VW')) { + getExtensionStart('governance/console').then( + () => { + if (this.mounted) { + this.setState({ governanceReady: true }); + } + }, + () => {} + ); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + handleNewProjectClick = (event: React.MouseEvent) => { event.preventDefault(); this.props.openProjectOnboarding(); }; - render() { + openCreatePortfolioForm = () => { + this.setState({ createPortfolio: true }); + }; + + closeCreatePortfolioForm = () => { + this.setState({ createPortfolio: false }); + }; + + handleNewPortfolioClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.openCreatePortfolioForm(); + }; + + handleCreatePortfolio = ({ key, qualifier }: { key: string; qualifier: string }) => { + this.closeCreatePortfolioForm(); + this.props.router.push(getPortfolioAdminUrl(key, qualifier)); + }; + + renderCreateProject() { + const { currentUser } = this.props; + if (!hasGlobalPermission(currentUser, 'provisioning')) { + return null; + } return ( - -
  • - - {translate('provisioning.create_new_project')} - -
  • -
  • -
  • - - {translate('my_account.create_new_organization')} - -
  • - - } - tagName="li"> - - +
  • + + {translate('provisioning.create_new_project')} - +
  • + ); + } + + renderCreateOrganization() { + if (!isSonarCloud()) { + return null; + } + + return ( +
  • + + {translate('my_account.create_new_organization')} + +
  • + ); + } + + renderCreatePortfolio(showGovernanceEntry: boolean, defaultQualifier?: string) { + const governanceInstalled = this.props.appState.qualifiers.includes('VW'); + if (!governanceInstalled || !showGovernanceEntry) { + return null; + } + + return ( +
  • + + {defaultQualifier + ? translate('my_account.create_new', defaultQualifier) + : translate('my_account.create_new_portfolio_application')} + +
  • + ); + } + + render() { + const { currentUser } = this.props; + const canCreateApplication = hasGlobalPermission(currentUser, 'applicationcreator'); + const canCreatePortfolio = hasGlobalPermission(currentUser, 'portfoliocreator'); + + let defaultQualifier: string | undefined; + if (!canCreateApplication) { + defaultQualifier = 'VW'; + } else if (!canCreatePortfolio) { + defaultQualifier = 'APP'; + } + + return ( + <> + + {this.renderCreateProject()} + {this.renderCreateOrganization()} + {this.renderCreatePortfolio( + canCreateApplication || canCreatePortfolio, + defaultQualifier + )} + + } + tagName="li"> + + + + + {this.state.governanceReady && + this.state.createPortfolio && ( + + )} + ); } } + +export default withRouter(GlobalNavPlus); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx index 88aaf4f172d..9305f1ddcbb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx @@ -18,23 +18,84 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { shallow } from 'enzyme'; -import GlobalNavPlus from '../GlobalNavPlus'; -import { click } from '../../../../../helpers/testUtils'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { GlobalNavPlus } from '../GlobalNavPlus'; +import { isSonarCloud } from '../../../../../helpers/system'; +import { click, mockRouter } from '../../../../../helpers/testUtils'; + +jest.mock('../../../../../helpers/system', () => ({ + isSonarCloud: jest.fn() +})); + +beforeEach(() => { + (isSonarCloud as jest.Mock).mockReturnValue(false); +}); it('render', () => { - const wrapper = shallow(); - expect(wrapper.is('Dropdown')).toBe(true); + const wrapper = getWrapper(); expect(wrapper.find('Dropdown')).toMatchSnapshot(); }); it('opens onboarding', () => { const openProjectOnboarding = jest.fn(); - const wrapper = shallow( - shallow() - .find('Dropdown') - .prop('overlay') - ); + const wrapper = getOverlayWrapper(getWrapper({ openProjectOnboarding })); click(wrapper.find('.js-new-project')); expect(openProjectOnboarding).toBeCalled(); }); + +it('should display create new project link when user has permission only', () => { + expect( + getOverlayWrapper(getWrapper({}, [])) + .find('.js-new-project') + .exists() + ).toBe(false); +}); + +it('should display create new organization on SonarCloud only', () => { + (isSonarCloud as jest.Mock).mockReturnValue(true); + expect(getOverlayWrapper(getWrapper())).toMatchSnapshot(); +}); + +it('should display create portfolio and application', () => { + checkOpenCreatePortfolio(['applicationcreator', 'portfoliocreator'], undefined); +}); + +it('should display create portfolio', () => { + checkOpenCreatePortfolio(['portfoliocreator'], 'VW'); +}); + +it('should display create application', () => { + checkOpenCreatePortfolio(['applicationcreator'], 'APP'); +}); + +function getWrapper(props = {}, globalPermissions?: string[]) { + return shallow( + + ); +} + +function getOverlayWrapper(wrapper: ShallowWrapper) { + return shallow(wrapper.find('Dropdown').prop('overlay')); +} + +function checkOpenCreatePortfolio(permissions: string[], defaultQualifier?: string) { + const wrapper = getWrapper({ appState: { qualifiers: ['VW'] } }, permissions); + wrapper.setState({ governanceReady: true }); + const overlayWrapper = getOverlayWrapper(wrapper); + expect(overlayWrapper.find('.js-new-portfolio')).toMatchSnapshot(); + + click(overlayWrapper.find('.js-new-portfolio')); + wrapper.update(); + expect(wrapper.find('CreateFormShim').exists()).toBe(true); + expect(wrapper.find('CreateFormShim').prop('defaultQualifier')).toBe(defaultQualifier); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap index 6598367a9cc..62c2757d57b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap @@ -39,13 +39,7 @@ exports[`should render for SonarCloud 1`] = ` } /> -
  • -
  • - - my_account.create_new_organization - -
  • } tagName="li" @@ -35,9 +22,65 @@ exports[`render 1`] = ` `; + +exports[`should display create application 1`] = ` + + my_account.create_new.APP + +`; + +exports[`should display create new organization on SonarCloud only 1`] = ` + +`; + +exports[`should display create portfolio 1`] = ` + + my_account.create_new.VW + +`; + +exports[`should display create portfolio and application 1`] = ` + + my_account.create_new_portfolio_application + +`; diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx new file mode 100644 index 00000000000..0bf661035b6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +interface Props { + defaultQualifier?: string; + onClose: () => void; + onCreate: (portfolio: { key: string; qualifier: string }) => void; +} + +export default class CreateFormShim extends React.Component { + render() { + const { CreateForm } = (window as any).SonarGovernance; + return ; + } +} diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 9d3959260bf..cc207fd5c2b 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -57,6 +57,10 @@ export function getPortfolioUrl(key: string): Location { return { pathname: '/portfolio', query: { id: key } }; } +export function getPortfolioAdminUrl(key: string, qualifier: string) { + return { pathname: '/project/admin/extension/governance/console', query: { id: key, qualifier } }; +} + export function getComponentBackgroundTaskUrl(componentKey: string, status?: string): Location { return { pathname: '/project/background_tasks', query: { id: componentKey, status } }; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index aaac8fa91d7..66508b5d588 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1492,8 +1492,12 @@ my_account.create_organization=Create Organization my_account.search_project=Search Project my_account.set_notifications_for=Set notifications for my_account.analyze_new_project=Analyze new project +my_account.create_new_portfolio_application=Create new portfolio / application +my_account.create_new.VW=Create new portfolio +my_account.create_new.APP=Create new application my_account.create_new_organization=Create new organization my_account.create_new_project_or_organization=Create new project or organization +my_account.create_new_project_portfolio_or_application=Create new project, portfolio or application #------------------------------------------------------------------------------ -- 2.39.5