]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13479 new creation menu
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 4 Jun 2020 07:35:00 +0000 (09:35 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 1 Jul 2020 20:05:53 +0000 (20:05 +0000)
14 files changed:
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/styles/components/menu.css
server/sonar-web/src/main/js/app/styles/init/misc.css
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/types.ts
server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2d3f41d9629bab25a387de3cbf290b7d5a82dc05..f94ebbb85740160ab9894ba6a2a19edda3a303c3 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Link, withRouter, WithRouterProps } from 'react-router';
 import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
 import PlusIcon from 'sonar-ui-common/components/icons/PlusIcon';
 import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getAlmSettings } from '../../../../api/alm-settings';
 import { getComponentNavigation } from '../../../../api/nav';
 import CreateFormShim from '../../../../apps/portfolio/components/CreateFormShim';
+import { Router, withRouter } from '../../../../components/hoc/withRouter';
 import { getExtensionStart } from '../../../../helpers/extensions';
-import { isSonarCloud } from '../../../../helpers/system';
 import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../helpers/urls';
 import { hasGlobalPermission } from '../../../../helpers/users';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ComponentQualifier } from '../../../../types/component';
+import GlobalNavPlusMenu from './GlobalNavPlusMenu';
 
 interface Props {
   appState: Pick<T.AppState, 'qualifiers'>;
   currentUser: T.LoggedInUser;
+  router: Router;
 }
 
 interface State {
-  createPortfolio: boolean;
+  boundAlms: Array<string>;
+  creatingComponent?: ComponentQualifier;
   governanceReady: boolean;
 }
 
-export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps, State> {
+/*
+ * ALMs for which the import feature has been implemented
+ */
+const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Bitbucket, AlmKeys.GitHub];
+
+export class GlobalNavPlus extends React.PureComponent<Props, State> {
   mounted = false;
-  state: State = { createPortfolio: false, governanceReady: false };
+  state: State = { boundAlms: [], governanceReady: false };
 
   componentDidMount() {
     this.mounted = true;
+
+    this.fetchAlmBindings();
+
     if (this.props.appState.qualifiers.includes('VW')) {
       getExtensionStart('governance/console').then(
         () => {
@@ -61,26 +74,31 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
     this.mounted = false;
   }
 
-  handleNewProjectClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    this.props.router.push('/projects/create');
+  closeComponentCreationForm = () => {
+    this.setState({ creatingComponent: undefined });
   };
 
-  openCreatePortfolioForm = () => {
-    this.setState({ createPortfolio: true });
-  };
+  fetchAlmBindings = async () => {
+    const almSettings = await getAlmSettings();
+
+    // Import is only available if exactly one binding is configured
+    const boundAlms = IMPORT_COMPATIBLE_ALMS.filter(key => {
+      const count = almSettings.filter(s => s.alm === key).length;
+      return count === 1;
+    });
 
-  closeCreatePortfolioForm = () => {
-    this.setState({ createPortfolio: false });
+    if (this.mounted) {
+      this.setState({
+        boundAlms
+      });
+    }
   };
 
-  handleNewPortfolioClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.openCreatePortfolioForm();
+  handleComponentCreationClick = (qualifier: ComponentQualifier) => {
+    this.setState({ creatingComponent: qualifier });
   };
 
-  handleCreatePortfolio = ({ key, qualifier }: { key: string; qualifier: string }) => {
+  handleComponentCreate = ({ key, qualifier }: { key: string; qualifier: ComponentQualifier }) => {
     return getComponentNavigation({ component: key }).then(data => {
       if (
         data.configuration &&
@@ -93,104 +111,50 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
       } else {
         this.props.router.push(getPortfolioUrl(key));
       }
-      this.closeCreatePortfolioForm();
+      this.closeComponentCreationForm();
     });
   };
 
-  renderCreateProject(canCreateProject: boolean) {
-    if (!canCreateProject) {
-      return null;
-    }
-    return (
-      <li>
-        <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
-          {isSonarCloud()
-            ? translate('provisioning.analyze_new_project')
-            : translate('my_account.create_new.TRK')}
-        </a>
-      </li>
-    );
-  }
-
-  renderCreateOrganization(canCreateOrg: boolean) {
-    if (!canCreateOrg) {
-      return null;
-    }
-
-    return (
-      <li>
-        <Link className="js-new-organization" to="/create-organization">
-          {translate('my_account.create_new_organization')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderCreatePortfolio(showGovernanceEntry: boolean, defaultQualifier?: string) {
-    const governanceInstalled = this.props.appState.qualifiers.includes('VW');
-    if (!governanceInstalled || !showGovernanceEntry) {
-      return null;
-    }
-
-    return (
-      <li>
-        <a className="js-new-portfolio" href="#" onClick={this.handleNewPortfolioClick}>
-          {defaultQualifier
-            ? translate('my_account.create_new', defaultQualifier)
-            : translate('my_account.create_new_portfolio_application')}
-        </a>
-      </li>
-    );
-  }
-
   render() {
-    const { currentUser } = this.props;
-    const canCreateApplication = hasGlobalPermission(currentUser, 'applicationcreator');
-    const canCreateOrg = isSonarCloud();
-    const canCreatePortfolio = hasGlobalPermission(currentUser, 'portfoliocreator');
-    const canCreateProject = isSonarCloud() || hasGlobalPermission(currentUser, 'provisioning');
-
-    if (!canCreateProject && !canCreateApplication && !canCreatePortfolio && !canCreateOrg) {
+    const { appState, currentUser } = this.props;
+    const { boundAlms, governanceReady, creatingComponent } = this.state;
+    const governanceInstalled = appState.qualifiers.includes(ComponentQualifier.Portfolio);
+    const canCreateApplication =
+      governanceInstalled && hasGlobalPermission(currentUser, 'applicationcreator');
+    const canCreatePortfolio =
+      governanceInstalled && hasGlobalPermission(currentUser, 'portfoliocreator');
+    const canCreateProject = hasGlobalPermission(currentUser, 'provisioning');
+
+    if (!canCreateProject && !canCreateApplication && !canCreatePortfolio) {
       return null;
     }
 
-    let defaultQualifier: string | undefined;
-    if (!canCreateApplication) {
-      defaultQualifier = 'VW';
-    } else if (!canCreatePortfolio) {
-      defaultQualifier = 'APP';
-    }
-
     return (
       <>
         <Dropdown
+          onOpen={canCreateProject ? this.fetchAlmBindings : undefined}
           overlay={
-            <ul className="menu">
-              {this.renderCreateProject(canCreateProject)}
-              {this.renderCreateOrganization(canCreateOrg)}
-              {this.renderCreatePortfolio(
-                canCreateApplication || canCreatePortfolio,
-                defaultQualifier
-              )}
-            </ul>
+            <GlobalNavPlusMenu
+              canCreateApplication={canCreateApplication}
+              canCreatePortfolio={canCreatePortfolio}
+              canCreateProject={canCreateProject}
+              compatibleAlms={boundAlms}
+              onComponentCreationClick={this.handleComponentCreationClick}
+            />
           }
           tagName="li">
           <a
             className="navbar-icon navbar-plus"
             href="#"
-            title={
-              isSonarCloud()
-                ? translate('my_account.create_new_project_or_organization')
-                : translate('my_account.create_new_project_portfolio_or_application')
-            }>
+            title={translate('my_account.create_new_project_portfolio_or_application')}>
             <PlusIcon />
           </a>
         </Dropdown>
-        {this.state.governanceReady && this.state.createPortfolio && (
+        {governanceReady && creatingComponent && (
           <CreateFormShim
-            defaultQualifier={defaultQualifier}
-            onClose={this.closeCreatePortfolioForm}
-            onCreate={this.handleCreatePortfolio}
+            defaultQualifier={creatingComponent}
+            onClose={this.closeComponentCreationForm}
+            onCreate={this.handleComponentCreate}
           />
         )}
       </>
@@ -198,4 +162,4 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
   }
 }
 
-export default withRouter<Props>(GlobalNavPlus);
+export default withRouter(GlobalNavPlus);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx
new file mode 100644 (file)
index 0000000..b3891fa
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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';
+import { Link } from 'react-router';
+import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import ChevronsIcon from 'sonar-ui-common/components/icons/ChevronsIcon';
+import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { ComponentQualifier } from '../../../../types/component';
+
+export interface GlobalNavPlusMenuProps {
+  canCreateApplication: boolean;
+  canCreatePortfolio: boolean;
+  canCreateProject: boolean;
+  compatibleAlms: Array<string>;
+  onComponentCreationClick: (componentQualifier: ComponentQualifier) => void;
+}
+
+function renderCreateProjectOptions(compatibleAlms: Array<string>) {
+  return [...compatibleAlms, 'manual'].map(alm => (
+    <li key={alm}>
+      <Link
+        className="display-flex-center"
+        to={{ pathname: '/projects/create', query: { mode: alm } }}>
+        {alm === 'manual' ? (
+          <ChevronsIcon className="spacer-right" />
+        ) : (
+          <img
+            alt={alm}
+            className="spacer-right"
+            width={16}
+            src={`${getBaseUrl()}/images/alm/${alm}.svg`}
+          />
+        )}
+        {translate('my_account.add_project', alm)}
+      </Link>
+    </li>
+  ));
+}
+
+function renderCreateComponent(
+  componentQualifier: ComponentQualifier,
+  onClick: (qualifier: ComponentQualifier) => void
+) {
+  return (
+    <li>
+      <ButtonLink
+        className="display-flex-justify-start padded-left"
+        onClick={() => onClick(componentQualifier)}>
+        <QualifierIcon className="spacer-right" qualifier={componentQualifier} />
+        {translate('my_account.create_new', componentQualifier)}
+      </ButtonLink>
+    </li>
+  );
+}
+
+export default function GlobalNavPlusMenu(props: GlobalNavPlusMenuProps) {
+  const { canCreateApplication, canCreatePortfolio, canCreateProject, compatibleAlms } = props;
+
+  return (
+    <ul className="menu">
+      {canCreateProject && (
+        <>
+          <li className="menu-header">
+            <strong>{translate('my_account.add_project')}</strong>
+          </li>
+          {renderCreateProjectOptions(compatibleAlms)}
+        </>
+      )}
+      {(canCreateApplication || canCreatePortfolio) && (
+        <>
+          {canCreateProject && <li className="divider" />}
+          {canCreatePortfolio &&
+            renderCreateComponent(ComponentQualifier.Portfolio, props.onComponentCreationClick)}
+          {canCreateApplication &&
+            renderCreateComponent(ComponentQualifier.Application, props.onComponentCreationClick)}
+        </>
+      )}
+    </ul>
+  );
+}
index 4914ea8d02ae306f3c85988aad9af0293c78dd78..62efab3775a43674ad23121d7427fbc7588890c9 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
 import * as React from 'react';
-import { click } from 'sonar-ui-common/helpers/testUtils';
-import { isSonarCloud } from '../../../../../helpers/system';
-import { mockRouter } from '../../../../../helpers/testMocks';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { getAlmSettings } from '../../../../../api/alm-settings';
+import { getComponentNavigation } from '../../../../../api/nav';
+import CreateFormShim from '../../../../../apps/portfolio/components/CreateFormShim';
+import { mockLoggedInUser, mockRouter } from '../../../../../helpers/testMocks';
+import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../../helpers/urls';
+import { AlmKeys } from '../../../../../types/alm-settings';
+import { ComponentQualifier } from '../../../../../types/component';
 import { GlobalNavPlus } from '../GlobalNavPlus';
 
-jest.mock('../../../../../helpers/system', () => ({
-  isSonarCloud: jest.fn()
+const PROJECT_CREATION_RIGHT = 'provisioning';
+const APP_CREATION_RIGHT = 'applicationcreator';
+const PORTFOLIO_CREATION_RIGHT = 'portfoliocreator';
+
+jest.mock('../../../../../api/alm-settings', () => ({
+  getAlmSettings: jest.fn().mockResolvedValue([])
 }));
 
-beforeEach(() => {
-  (isSonarCloud as jest.Mock).mockReturnValue(false);
-});
+jest.mock('../../../../../api/nav', () => ({
+  getComponentNavigation: jest.fn().mockResolvedValue({})
+}));
 
-it('render', () => {
-  const wrapper = getWrapper();
-  expect(wrapper.find('Dropdown')).toMatchSnapshot();
-});
+jest.mock('../../../../../helpers/urls', () => ({
+  getPortfolioUrl: jest.fn(),
+  getPortfolioAdminUrl: jest.fn()
+}));
 
-it('opens onboarding', () => {
-  const push = jest.fn();
-  const wrapper = getOverlayWrapper(getWrapper({ router: mockRouter({ push }) }));
-  click(wrapper.find('.js-new-project'));
-  expect(push).toBeCalled();
-});
+it('should render correctly', () => {
+  expect(shallowRender().type()).toBeNull();
+  expect(
+    shallowRender([APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT])
+  ).toMatchSnapshot('no governance');
 
-it('should display create new project link when user has permission only', () => {
-  expect(getWrapper({}, []).find('Dropdown').length).toEqual(0);
+  const wrapper = shallowRender(
+    [APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT],
+    true
+  );
+  wrapper.setState({ boundAlms: ['bitbucket'] });
+  expect(wrapper).toMatchSnapshot('full rights and alms');
 });
 
-it('should display create new organization on SonarCloud only', () => {
-  (isSonarCloud as jest.Mock).mockReturnValue(true);
-  expect(getOverlayWrapper(getWrapper())).toMatchSnapshot();
-});
+it('should load correctly', async () => {
+  (getAlmSettings as jest.Mock).mockResolvedValueOnce([
+    { alm: AlmKeys.Azure, key: 'A1' },
+    { alm: AlmKeys.Bitbucket, key: 'B1' },
+    { alm: AlmKeys.GitHub, key: 'GH1' }
+  ]);
 
-it('should display new organization and new project on SonarCloud', () => {
-  (isSonarCloud as jest.Mock).mockReturnValue(true);
-  expect(getOverlayWrapper(getWrapper({}, []))).toMatchSnapshot();
-});
+  const wrapper = shallowRender();
 
-it('should display create portfolio and application', () => {
-  checkOpenCreatePortfolio(['applicationcreator', 'portfoliocreator'], undefined);
+  await waitAndUpdate(wrapper);
+
+  expect(getAlmSettings).toBeCalled();
+  expect(wrapper.state().boundAlms).toEqual([AlmKeys.Bitbucket, AlmKeys.GitHub]);
 });
 
-it('should display create portfolio', () => {
-  checkOpenCreatePortfolio(['portfoliocreator'], 'VW');
+it('should display component creation form', () => {
+  const wrapper = shallowRender([PORTFOLIO_CREATION_RIGHT], true);
+
+  wrapper.instance().handleComponentCreationClick(ComponentQualifier.Portfolio);
+  wrapper.setState({ governanceReady: true });
+
+  expect(wrapper.find(CreateFormShim).exists()).toBe(true);
 });
 
-it('should display create application', () => {
-  checkOpenCreatePortfolio(['applicationcreator'], 'APP');
+describe('handleComponentCreate', () => {
+  (getComponentNavigation as jest.Mock)
+    .mockResolvedValueOnce({
+      configuration: { extensions: [{ key: 'governance/console', name: 'governance' }] }
+    })
+    .mockResolvedValueOnce({});
+
+  const portfolio = { key: 'portfolio', qualifier: ComponentQualifier.Portfolio };
+
+  const wrapper = shallowRender([], true);
+
+  it('should redirect to admin', async () => {
+    wrapper.instance().handleComponentCreate(portfolio);
+    await waitAndUpdate(wrapper);
+    expect(getPortfolioAdminUrl).toBeCalledWith(portfolio.key, portfolio.qualifier);
+    expect(wrapper.state().creatingComponent).toBeUndefined();
+  });
+
+  it('should redirect to dashboard', async () => {
+    wrapper.instance().handleComponentCreate(portfolio);
+    await waitAndUpdate(wrapper);
+
+    expect(getPortfolioUrl).toBeCalledWith(portfolio.key);
+  });
 });
 
-function getWrapper(props = {}, globalPermissions?: string[]) {
-  return shallow(
-    // @ts-ignore avoid passing everything from WithRouterProps
+function shallowRender(permissions: string[] = [], enableGovernance = false) {
+  return shallow<GlobalNavPlus>(
     <GlobalNavPlus
-      appState={{ qualifiers: [] }}
-      currentUser={
-        {
-          isLoggedIn: true,
-          permissions: { global: globalPermissions || ['provisioning'] }
-        } as T.LoggedInUser
-      }
+      appState={{ qualifiers: enableGovernance ? [ComponentQualifier.Portfolio] : [] }}
+      currentUser={mockLoggedInUser({ permissions: { global: permissions } })}
       router={mockRouter()}
-      {...props}
     />
   );
 }
-
-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__/GlobalNavPlusMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx
new file mode 100644 (file)
index 0000000..4eb0f37
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import { AlmKeys } from '../../../../../types/alm-settings';
+import { ComponentQualifier } from '../../../../../types/component';
+import GlobalNavPlusMenu, { GlobalNavPlusMenuProps } from '../GlobalNavPlusMenu';
+
+it('should render correctly', () => {
+  expect(shallowRender({ canCreateApplication: true })).toMatchSnapshot('app only');
+  expect(shallowRender({ canCreatePortfolio: true })).toMatchSnapshot('portfolio only');
+  expect(shallowRender({ canCreateProject: true })).toMatchSnapshot('project only');
+  expect(
+    shallowRender({ canCreateProject: true, compatibleAlms: [AlmKeys.Bitbucket] })
+  ).toMatchSnapshot('imports');
+  expect(
+    shallowRender({
+      canCreateApplication: true,
+      canCreatePortfolio: true,
+      canCreateProject: true,
+      compatibleAlms: [AlmKeys.Bitbucket]
+    })
+  ).toMatchSnapshot('all');
+});
+
+it('should trigger onClick', () => {
+  const onComponentCreationClick = jest.fn();
+  const wrapper = shallowRender({
+    canCreateApplication: true,
+    canCreatePortfolio: true,
+    onComponentCreationClick
+  });
+
+  // Portfolio
+  const portfolioButton = wrapper.find(ButtonLink).at(0);
+  portfolioButton.simulate('click');
+  expect(onComponentCreationClick).toBeCalledWith(ComponentQualifier.Portfolio);
+
+  onComponentCreationClick.mockClear();
+
+  // App
+  const appButton = wrapper.find(ButtonLink).at(1);
+  appButton.simulate('click');
+  expect(onComponentCreationClick).toBeCalledWith(ComponentQualifier.Application);
+});
+
+function shallowRender(overrides: Partial<GlobalNavPlusMenuProps> = {}) {
+  return shallow(
+    <GlobalNavPlusMenu
+      canCreateApplication={false}
+      canCreatePortfolio={false}
+      canCreateProject={false}
+      compatibleAlms={[]}
+      onComponentCreationClick={jest.fn()}
+      {...overrides}
+    />
+  );
+}
index 11427944e710f96796eefa8e8890ca0e338c200f..658c06d2ab923719f4869c08bf9eab3ff5aed995 100644 (file)
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`render 1`] = `
-<Dropdown
-  overlay={
-    <ul
-      className="menu"
-    >
-      <li>
-        <a
-          className="js-new-project"
-          href="#"
-          onClick={[Function]}
-        >
-          my_account.create_new.TRK
-        </a>
-      </li>
-    </ul>
-  }
-  tagName="li"
->
-  <a
-    className="navbar-icon navbar-plus"
-    href="#"
-    title="my_account.create_new_project_portfolio_or_application"
+exports[`should render correctly: full rights and alms 1`] = `
+<Fragment>
+  <Dropdown
+    onOpen={[Function]}
+    overlay={
+      <GlobalNavPlusMenu
+        canCreateApplication={true}
+        canCreatePortfolio={true}
+        canCreateProject={true}
+        compatibleAlms={
+          Array [
+            "bitbucket",
+          ]
+        }
+        onComponentCreationClick={[Function]}
+      />
+    }
+    tagName="li"
   >
-    <PlusIcon />
-  </a>
-</Dropdown>
-`;
-
-exports[`should display create application 1`] = `
-<a
-  className="js-new-portfolio"
-  href="#"
-  onClick={[Function]}
->
-  my_account.create_new.APP
-</a>
-`;
-
-exports[`should display create new organization on SonarCloud only 1`] = `
-<ul
-  className="menu"
->
-  <li>
     <a
-      className="js-new-project"
+      className="navbar-icon navbar-plus"
       href="#"
-      onClick={[Function]}
+      title="my_account.create_new_project_portfolio_or_application"
     >
-      provisioning.analyze_new_project
+      <PlusIcon />
     </a>
-  </li>
-  <li>
-    <Link
-      className="js-new-organization"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/create-organization"
-    >
-      my_account.create_new_organization
-    </Link>
-  </li>
-</ul>
+  </Dropdown>
+</Fragment>
 `;
 
-exports[`should display create portfolio 1`] = `
-<a
-  className="js-new-portfolio"
-  href="#"
-  onClick={[Function]}
->
-  my_account.create_new.VW
-</a>
-`;
-
-exports[`should display create portfolio and application 1`] = `
-<a
-  className="js-new-portfolio"
-  href="#"
-  onClick={[Function]}
->
-  my_account.create_new_portfolio_application
-</a>
-`;
-
-exports[`should display new organization and new project on SonarCloud 1`] = `
-<ul
-  className="menu"
->
-  <li>
+exports[`should render correctly: no governance 1`] = `
+<Fragment>
+  <Dropdown
+    onOpen={[Function]}
+    overlay={
+      <GlobalNavPlusMenu
+        canCreateApplication={false}
+        canCreatePortfolio={false}
+        canCreateProject={true}
+        compatibleAlms={Array []}
+        onComponentCreationClick={[Function]}
+      />
+    }
+    tagName="li"
+  >
     <a
-      className="js-new-project"
+      className="navbar-icon navbar-plus"
       href="#"
-      onClick={[Function]}
+      title="my_account.create_new_project_portfolio_or_application"
     >
-      provisioning.analyze_new_project
+      <PlusIcon />
     </a>
-  </li>
-  <li>
-    <Link
-      className="js-new-organization"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/create-organization"
-    >
-      my_account.create_new_organization
-    </Link>
-  </li>
-</ul>
+  </Dropdown>
+</Fragment>
 `;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap
new file mode 100644 (file)
index 0000000..e10a9f2
--- /dev/null
@@ -0,0 +1,224 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: all 1`] = `
+<ul
+  className="menu"
+>
+  <li
+    className="menu-header"
+  >
+    <strong>
+      my_account.add_project
+    </strong>
+  </li>
+  <li
+    key="bitbucket"
+  >
+    <Link
+      className="display-flex-center"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/projects/create",
+          "query": Object {
+            "mode": "bitbucket",
+          },
+        }
+      }
+    >
+      <img
+        alt="bitbucket"
+        className="spacer-right"
+        src="/images/alm/bitbucket.svg"
+        width={16}
+      />
+      my_account.add_project.bitbucket
+    </Link>
+  </li>
+  <li
+    key="manual"
+  >
+    <Link
+      className="display-flex-center"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/projects/create",
+          "query": Object {
+            "mode": "manual",
+          },
+        }
+      }
+    >
+      <ChevronsIcon
+        className="spacer-right"
+      />
+      my_account.add_project.manual
+    </Link>
+  </li>
+  <li
+    className="divider"
+  />
+  <li>
+    <ButtonLink
+      className="display-flex-justify-start padded-left"
+      onClick={[Function]}
+    >
+      <QualifierIcon
+        className="spacer-right"
+        qualifier="VW"
+      />
+      my_account.create_new.VW
+    </ButtonLink>
+  </li>
+  <li>
+    <ButtonLink
+      className="display-flex-justify-start padded-left"
+      onClick={[Function]}
+    >
+      <QualifierIcon
+        className="spacer-right"
+        qualifier="APP"
+      />
+      my_account.create_new.APP
+    </ButtonLink>
+  </li>
+</ul>
+`;
+
+exports[`should render correctly: app only 1`] = `
+<ul
+  className="menu"
+>
+  <li>
+    <ButtonLink
+      className="display-flex-justify-start padded-left"
+      onClick={[Function]}
+    >
+      <QualifierIcon
+        className="spacer-right"
+        qualifier="APP"
+      />
+      my_account.create_new.APP
+    </ButtonLink>
+  </li>
+</ul>
+`;
+
+exports[`should render correctly: imports 1`] = `
+<ul
+  className="menu"
+>
+  <li
+    className="menu-header"
+  >
+    <strong>
+      my_account.add_project
+    </strong>
+  </li>
+  <li
+    key="bitbucket"
+  >
+    <Link
+      className="display-flex-center"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/projects/create",
+          "query": Object {
+            "mode": "bitbucket",
+          },
+        }
+      }
+    >
+      <img
+        alt="bitbucket"
+        className="spacer-right"
+        src="/images/alm/bitbucket.svg"
+        width={16}
+      />
+      my_account.add_project.bitbucket
+    </Link>
+  </li>
+  <li
+    key="manual"
+  >
+    <Link
+      className="display-flex-center"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/projects/create",
+          "query": Object {
+            "mode": "manual",
+          },
+        }
+      }
+    >
+      <ChevronsIcon
+        className="spacer-right"
+      />
+      my_account.add_project.manual
+    </Link>
+  </li>
+</ul>
+`;
+
+exports[`should render correctly: portfolio only 1`] = `
+<ul
+  className="menu"
+>
+  <li>
+    <ButtonLink
+      className="display-flex-justify-start padded-left"
+      onClick={[Function]}
+    >
+      <QualifierIcon
+        className="spacer-right"
+        qualifier="VW"
+      />
+      my_account.create_new.VW
+    </ButtonLink>
+  </li>
+</ul>
+`;
+
+exports[`should render correctly: project only 1`] = `
+<ul
+  className="menu"
+>
+  <li
+    className="menu-header"
+  >
+    <strong>
+      my_account.add_project
+    </strong>
+  </li>
+  <li
+    key="manual"
+  >
+    <Link
+      className="display-flex-center"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/projects/create",
+          "query": Object {
+            "mode": "manual",
+          },
+        }
+      }
+    >
+      <ChevronsIcon
+        className="spacer-right"
+      />
+      my_account.add_project.manual
+    </Link>
+  </li>
+</ul>
+`;
index 2062096a1de5dc5529c29ebf075c287dfd9fc49a..64d39525208e995a0ec84b8673761509f06f6932 100644 (file)
@@ -37,6 +37,7 @@
 
 .menu-item,
 .menu > li > a,
+.menu > li > button,
 .menu > li > span {
   display: block;
   padding: 4px 16px;
   transition: none;
 }
 
+.menu > li > button {
+  color: var(--baseFontColor);
+  text-align: left;
+  width: 100%;
+}
+
 .menu > li > a.rich-item {
   display: flex;
   align-items: center;
@@ -82,7 +89,9 @@
 }
 
 .menu > li > a:hover,
-.menu > li > a:focus {
+.menu > li > a:focus,
+.menu > li > button:hover,
+.menu > li > button:focus {
   text-decoration: none;
   color: var(--baseFontColor);
   background-color: var(--barBackgroundColor);
index b05d35cd039822f6bb9e066730ed45c98e18c379..b987a0656534c1ee02e0cf43cf8b88644a475ac7 100644 (file)
@@ -395,6 +395,11 @@ th.huge-spacer-right {
   align-items: center;
 }
 
+.display-flex-justify-start {
+  display: flex !important;
+  justify-content: flex-start !important;
+}
+
 .display-flex-justify-center {
   display: flex !important;
   justify-content: center;
index 6ba025c784ece6a2f4cf1eb4f80bde731b3982c3..fa7fa8fc11da695d6044210b797dbeb0413f86ec 100644 (file)
@@ -68,7 +68,7 @@ exports[`should render correctly: no projects 1`] = `
             Object {
               "pathname": "/projects/create",
               "query": Object {
-                "mode": "bbs",
+                "mode": "bitbucket",
                 "resetPat": 1,
               },
             }
index ced072128a27d30bd896a6056992baa2fc08fa22..03b1ab6bdf0c5a141c87bf91036bd7c40d9435e8 100644 (file)
@@ -263,7 +263,7 @@ exports[`should render correctly: no repos 1`] = `
                 Object {
                   "pathname": "/projects/create",
                   "query": Object {
-                    "mode": "bbs",
+                    "mode": "bitbucket",
                     "resetPat": 1,
                   },
                 }
index 05531913bb2c922b841ffea584563084a4e72681..99b42043c24cf870428027d1189483639bc2e056 100644 (file)
@@ -81,7 +81,7 @@ exports[`should render correctly if the BBS method is selected 1`] = `
           "key": "key",
           "pathname": "/path",
           "query": Object {
-            "mode": "bbs",
+            "mode": "bitbucket",
           },
           "search": "",
           "state": Object {},
index b316a4e781a24542152c9a3209ca541116467580..d991cd60d3a5f2fbc51826d921a54d0ad96c37c9 100644 (file)
@@ -19,5 +19,5 @@
  */
 export enum CreateProjectModes {
   Manual = 'manual',
-  BitbucketServer = 'bbs'
+  BitbucketServer = 'bitbucket'
 }
index 7dd89ec8cc2660a005a60d52f6897f744e2ee463..96f9ab98e8631e0189dcd7d2edfac81d608eff70 100644 (file)
@@ -21,11 +21,12 @@ import * as React from 'react';
 import * as theme from '../../../app/theme';
 import { getCurrentL10nBundle } from '../../../helpers/l10n';
 import { getBaseUrl } from '../../../helpers/system';
+import { ComponentQualifier } from '../../../types/component';
 
 interface Props {
   defaultQualifier?: string;
   onClose: () => void;
-  onCreate: (portfolio: { key: string; qualifier: string }) => void;
+  onCreate: (portfolio: { key: string; qualifier: ComponentQualifier }) => void;
 }
 
 export default class CreateFormShim extends React.Component<Props> {
index 5aafe022028e3a6a83c5da6671a9bddfc04bfd05..03d6729ba44b81ab76c9edd8f93f349622589e54 100644 (file)
@@ -1787,12 +1787,14 @@ my_account.create_organization=Create Organization
 my_account.search_project=Search Project
 my_account.set_notifications_for=Search a project by name
 my_account.set_notifications_for.title=Add a project
-my_account.create_new_portfolio_application=Create new portfolio / application
 my_account.create_new.TRK=Create new project
-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=Analyze new project or create new organization
+my_account.create_new.VW=Create portfolio
+my_account.create_new.APP=Create application
+my_account.add_project=Add project
+my_account.add_project.manual=Manually
+my_account.add_project.github=GitHub
+my_account.add_project.bitbucket=Bitbucket
+
 my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application
 
 
@@ -1801,7 +1803,6 @@ my_account.create_new_project_portfolio_or_application=Analyze new project / Cre
 # PROJECT PROVISIONING
 #
 #------------------------------------------------------------------------------
-provisioning.analyze_new_project=Analyze new project
 provisioning.no_analysis=No analysis has been performed since creation. The only available section is the configuration.
 provisioning.no_analysis.delete=Either you should retry analysis or simply {link}.
 provisioning.no_analysis.delete_project=delete the project