]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13887 Move creation menu
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 13 Jan 2021 17:00:14 +0000 (18:00 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 26 Jan 2021 20:07:55 +0000 (20:07 +0000)
28 files changed:
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx [deleted file]
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/CreateFormShim-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/FavoriteFilter-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/styles.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 10881c30042ba4b901f66def01c62590a64fb149..bf07d907cf91322806ae959fe74136f825e81d21 100644 (file)
@@ -19,9 +19,7 @@
  */
 import * as React from 'react';
 import { connect } from 'react-redux';
-import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';
 import NavBar from 'sonar-ui-common/components/ui/NavBar';
-import { isLoggedIn } from '../../../../helpers/users';
 import { getAppState, getCurrentUser, Store } from '../../../../store/rootReducer';
 import { rawSizes } from '../../../theme';
 import EmbedDocsPopupHelper from '../../embed-docs-modal/EmbedDocsPopupHelper';
@@ -31,8 +29,6 @@ import GlobalNavBranding from './GlobalNavBranding';
 import GlobalNavMenu from './GlobalNavMenu';
 import GlobalNavUser from './GlobalNavUser';
 
-const GlobalNavPlus = lazyLoadComponent(() => import('./GlobalNavPlus'), 'GlobalNavPlus');
-
 export interface GlobalNavProps {
   appState: Pick<T.AppState, 'canAdmin' | 'globalPages' | 'qualifiers'>;
   currentUser: T.CurrentUser;
@@ -50,7 +46,6 @@ export function GlobalNav(props: GlobalNavProps) {
       <ul className="global-navbar-menu global-navbar-menu-right">
         <EmbedDocsPopupHelper />
         <Search currentUser={currentUser} />
-        {isLoggedIn(currentUser) && <GlobalNavPlus appState={appState} currentUser={currentUser} />}
         <GlobalNavUser currentUser={currentUser} />
       </ul>
     </NavBar>
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
deleted file mode 100644 (file)
index cd16499..0000000
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 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 { getComponentAdminUrl, getComponentOverviewUrl } from '../../../../helpers/urls';
-import { hasGlobalPermission } from '../../../../helpers/users';
-import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
-import { ComponentQualifier } from '../../../../types/component';
-import CreateApplicationForm from '../../extensions/CreateApplicationForm';
-import GlobalNavPlusMenu from './GlobalNavPlusMenu';
-
-interface Props {
-  appState: Pick<T.AppState, 'branchesEnabled' | 'qualifiers'>;
-  currentUser: T.LoggedInUser;
-  router: Router;
-}
-
-interface State {
-  boundAlms: Array<string>;
-  creatingComponent?: ComponentQualifier;
-  governanceReady: boolean;
-}
-
-/*
- * ALMs for which the import feature has been implemented
- */
-const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Azure, AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];
-
-const almSettingsValidators = {
-  [AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url,
-  [AlmKeys.Bitbucket]: (_: AlmSettingsInstance) => true,
-  [AlmKeys.GitHub]: (_: AlmSettingsInstance) => true,
-  [AlmKeys.GitLab]: (settings: AlmSettingsInstance) => !!settings.url
-};
-
-export class GlobalNavPlus extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { boundAlms: [], governanceReady: false };
-
-  componentDidMount() {
-    this.mounted = true;
-
-    this.fetchAlmBindings();
-
-    if (this.props.appState.qualifiers.includes(ComponentQualifier.Portfolio)) {
-      getExtensionStart('governance/console').then(
-        () => {
-          if (this.mounted) {
-            this.setState({ governanceReady: true });
-          }
-        },
-        () => {
-          /* error handled globally */
-        }
-      );
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  closeComponentCreationForm = () => {
-    this.setState({ creatingComponent: undefined });
-  };
-
-  almSettingIsValid = (settings: AlmSettingsInstance) => {
-    return almSettingsValidators[settings.alm](settings);
-  };
-
-  fetchAlmBindings = async () => {
-    const {
-      appState: { branchesEnabled },
-      currentUser
-    } = this.props;
-    const canCreateProject = hasGlobalPermission(currentUser, 'provisioning');
-
-    // getAlmSettings requires branchesEnabled
-    if (!canCreateProject || !branchesEnabled) {
-      return;
-    }
-
-    const almSettings = await getAlmSettings();
-
-    // Import is only available if exactly one binding is configured
-    const boundAlms = IMPORT_COMPATIBLE_ALMS.filter(key => {
-      const currentAlmSettings = almSettings.filter(s => s.alm === key);
-      return currentAlmSettings.length === 1 && this.almSettingIsValid(currentAlmSettings[0]);
-    });
-
-    if (this.mounted) {
-      this.setState({
-        boundAlms
-      });
-    }
-  };
-
-  handleComponentCreationClick = (qualifier: ComponentQualifier) => {
-    this.setState({ creatingComponent: qualifier });
-  };
-
-  handleComponentCreate = ({ key, qualifier }: { key: string; qualifier: ComponentQualifier }) => {
-    return getComponentNavigation({ component: key }).then(({ configuration }) => {
-      if (configuration && configuration.showSettings) {
-        this.props.router.push(getComponentAdminUrl(key, qualifier));
-      } else {
-        this.props.router.push(getComponentOverviewUrl(key, qualifier));
-      }
-      this.closeComponentCreationForm();
-    });
-  };
-
-  render() {
-    const { appState, currentUser } = this.props;
-    const { boundAlms, governanceReady, creatingComponent } = this.state;
-    const canCreateApplication =
-      appState.qualifiers.includes(ComponentQualifier.Application) &&
-      hasGlobalPermission(currentUser, 'applicationcreator');
-    const canCreatePortfolio =
-      appState.qualifiers.includes(ComponentQualifier.Portfolio) &&
-      hasGlobalPermission(currentUser, 'portfoliocreator');
-    const canCreateProject = hasGlobalPermission(currentUser, 'provisioning');
-
-    if (!canCreateProject && !canCreateApplication && !canCreatePortfolio) {
-      return null;
-    }
-
-    return (
-      <>
-        <Dropdown
-          onOpen={this.fetchAlmBindings}
-          overlay={
-            <GlobalNavPlusMenu
-              canCreateApplication={canCreateApplication}
-              canCreatePortfolio={canCreatePortfolio}
-              canCreateProject={canCreateProject}
-              compatibleAlms={boundAlms}
-              onComponentCreationClick={this.handleComponentCreationClick}
-            />
-          }
-          tagName="li">
-          <a
-            className="navbar-icon navbar-plus"
-            href="#"
-            title={translate('my_account.create_new_project_portfolio_or_application')}>
-            <PlusIcon />
-          </a>
-        </Dropdown>
-
-        {canCreateApplication && creatingComponent === ComponentQualifier.Application && (
-          <CreateApplicationForm
-            onClose={this.closeComponentCreationForm}
-            onCreate={this.handleComponentCreate}
-          />
-        )}
-
-        {governanceReady && creatingComponent === ComponentQualifier.Portfolio && (
-          <CreateFormShim
-            defaultQualifier={creatingComponent}
-            onClose={this.closeComponentCreationForm}
-            onCreate={this.handleComponentCreate}
-          />
-        )}
-      </>
-    );
-  }
-}
-
-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
deleted file mode 100644 (file)
index f1f7719..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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>
-  );
-}
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
deleted file mode 100644 (file)
index d637a46..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { 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 { getComponentAdminUrl, getComponentOverviewUrl } from '../../../../../helpers/urls';
-import { AlmKeys } from '../../../../../types/alm-settings';
-import { ComponentQualifier } from '../../../../../types/component';
-import { GlobalNavPlus } from '../GlobalNavPlus';
-
-const PROJECT_CREATION_RIGHT = 'provisioning';
-const APP_CREATION_RIGHT = 'applicationcreator';
-const PORTFOLIO_CREATION_RIGHT = 'portfoliocreator';
-
-jest.mock('../../../../../helpers/extensions', () => ({
-  getExtensionStart: jest.fn().mockResolvedValue(null)
-}));
-
-jest.mock('../../../../../api/alm-settings', () => ({
-  getAlmSettings: jest.fn().mockResolvedValue([])
-}));
-
-jest.mock('../../../../../api/nav', () => ({
-  getComponentNavigation: jest.fn().mockResolvedValue({})
-}));
-
-jest.mock('../../../../../helpers/urls', () => ({
-  getComponentOverviewUrl: jest.fn(),
-  getComponentAdminUrl: jest.fn()
-}));
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-it('should render correctly when no rights', async () => {
-  const wrapper = shallowRender([], {});
-  expect(wrapper.type()).toBeNull();
-  await waitAndUpdate(wrapper);
-  expect(getAlmSettings).not.toBeCalled();
-});
-
-it('should render correctly if branches not enabled', async () => {
-  const wrapper = shallowRender([PROJECT_CREATION_RIGHT], { branchesEnabled: false });
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-  expect(getAlmSettings).not.toBeCalled();
-});
-
-it('should render correctly', async () => {
-  expect(
-    shallowRender([APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT], {})
-  ).toMatchSnapshot('no governance');
-
-  const wrapper = shallowRender(
-    [APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT],
-    { enableGovernance: true }
-  );
-  await waitAndUpdate(wrapper);
-  wrapper.setState({ boundAlms: ['bitbucket'] });
-  expect(wrapper).toMatchSnapshot('full rights and alms');
-});
-
-it('should load correctly', async () => {
-  (getAlmSettings as jest.Mock).mockResolvedValueOnce([
-    { alm: AlmKeys.Azure, key: 'A1' }, // No azure onboarding for now
-    { alm: AlmKeys.Bitbucket, key: 'B1' },
-    { alm: AlmKeys.GitHub, key: 'GH1' },
-    { alm: AlmKeys.GitLab, key: 'GL1', url: 'ok' }
-  ]);
-
-  const wrapper = shallowRender([PROJECT_CREATION_RIGHT], {});
-
-  await waitAndUpdate(wrapper);
-
-  expect(getAlmSettings).toBeCalled();
-  expect(wrapper.state().boundAlms).toEqual([AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab]);
-});
-
-it('should load without gitlab when no url', async () => {
-  (getAlmSettings as jest.Mock).mockResolvedValueOnce([{ alm: AlmKeys.GitLab, key: 'GL1' }]);
-
-  const wrapper = shallowRender([PROJECT_CREATION_RIGHT], {});
-
-  await waitAndUpdate(wrapper);
-
-  expect(getAlmSettings).toBeCalled();
-  expect(wrapper.state().boundAlms).toEqual([]);
-});
-
-it('should display component creation form', () => {
-  const wrapper = shallowRender([PORTFOLIO_CREATION_RIGHT], { enableGovernance: true });
-
-  wrapper.instance().handleComponentCreationClick(ComponentQualifier.Portfolio);
-  wrapper.setState({ governanceReady: true });
-
-  expect(wrapper.find(CreateFormShim).exists()).toBe(true);
-});
-
-describe('handleComponentCreate', () => {
-  (getComponentNavigation as jest.Mock)
-    .mockResolvedValueOnce({
-      configuration: { showSettings: true }
-    })
-    .mockResolvedValueOnce({});
-
-  const portfolio = { key: 'portfolio', qualifier: ComponentQualifier.Portfolio };
-
-  const wrapper = shallowRender([], { enableGovernance: true });
-
-  it('should redirect to admin', async () => {
-    wrapper.instance().handleComponentCreate(portfolio);
-    await waitAndUpdate(wrapper);
-    expect(getComponentAdminUrl).toBeCalledWith(portfolio.key, portfolio.qualifier);
-    expect(wrapper.state().creatingComponent).toBeUndefined();
-  });
-
-  it('should redirect to dashboard', async () => {
-    wrapper.instance().handleComponentCreate(portfolio);
-    await waitAndUpdate(wrapper);
-
-    expect(getComponentOverviewUrl).toBeCalledWith(portfolio.key, portfolio.qualifier);
-  });
-});
-
-function shallowRender(
-  permissions: string[] = [],
-  { enableGovernance = false, branchesEnabled = true }
-) {
-  let qualifiers: ComponentQualifier[];
-  if (enableGovernance) {
-    qualifiers = [ComponentQualifier.Portfolio, ComponentQualifier.Application];
-  } else if (branchesEnabled) {
-    qualifiers = [ComponentQualifier.Application];
-  } else {
-    qualifiers = [];
-  }
-  return shallow<GlobalNavPlus>(
-    <GlobalNavPlus
-      appState={{
-        branchesEnabled,
-        qualifiers
-      }}
-      currentUser={mockLoggedInUser({ permissions: { global: permissions } })}
-      router={mockRouter()}
-    />
-  );
-}
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
deleted file mode 100644 (file)
index 1f31f8d..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 df3ede93b16229993c41e72bc43b9d51e617f008..d3f0165206c588fbf35553c4d9271c6c547c8e7c 100644 (file)
@@ -85,20 +85,6 @@ exports[`should render correctly: logged in users 1`] = `
         }
       }
     />
-    <GlobalNavPlus
-      appState={
-        Object {
-          "canAdmin": false,
-          "globalPages": Array [],
-          "qualifiers": Array [],
-        }
-      }
-      currentUser={
-        Object {
-          "isLoggedIn": true,
-        }
-      }
-    />
     <withRouter(GlobalNavUser)
       currentUser={
         Object {
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
deleted file mode 100644 (file)
index e68e1c1..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly if branches not enabled 1`] = `
-<Fragment>
-  <Dropdown
-    onOpen={[Function]}
-    overlay={
-      <GlobalNavPlusMenu
-        canCreateApplication={false}
-        canCreatePortfolio={false}
-        canCreateProject={true}
-        compatibleAlms={Array []}
-        onComponentCreationClick={[Function]}
-      />
-    }
-    tagName="li"
-  >
-    <a
-      className="navbar-icon navbar-plus"
-      href="#"
-      title="my_account.create_new_project_portfolio_or_application"
-    >
-      <PlusIcon />
-    </a>
-  </Dropdown>
-</Fragment>
-`;
-
-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"
-  >
-    <a
-      className="navbar-icon navbar-plus"
-      href="#"
-      title="my_account.create_new_project_portfolio_or_application"
-    >
-      <PlusIcon />
-    </a>
-  </Dropdown>
-</Fragment>
-`;
-
-exports[`should render correctly: no governance 1`] = `
-<Fragment>
-  <Dropdown
-    onOpen={[Function]}
-    overlay={
-      <GlobalNavPlusMenu
-        canCreateApplication={true}
-        canCreatePortfolio={false}
-        canCreateProject={true}
-        compatibleAlms={Array []}
-        onComponentCreationClick={[Function]}
-      />
-    }
-    tagName="li"
-  >
-    <a
-      className="navbar-icon navbar-plus"
-      href="#"
-      title="my_account.create_new_project_portfolio_or_application"
-    >
-      <PlusIcon />
-    </a>
-  </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
deleted file mode 100644 (file)
index e10a9f2..0000000
+++ /dev/null
@@ -1,224 +0,0 @@
-// 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>
-`;
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
deleted file mode 100644 (file)
index e4efe80..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 * 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: ComponentQualifier }) => void;
-}
-
-export default class CreateFormShim extends React.Component<Props> {
-  render() {
-    const { createFormBuilder } = (window as any).SonarGovernance;
-    return createFormBuilder(this.props, {
-      theme,
-      baseUrl: getBaseUrl(),
-      l10nBundle: getCurrentL10nBundle()
-    });
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/CreateFormShim-test.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/CreateFormShim-test.tsx
deleted file mode 100644 (file)
index 2228bfa..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 CreateFormShim from '../CreateFormShim';
-
-afterAll(() => delete (window as any).SonarGovernance);
-
-it('should call SonarGovernance createFormBuilder to build CreateForm component', () => {
-  const builderMock = jest.fn();
-  (window as any).SonarGovernance = { createFormBuilder: builderMock };
-  shallowRender();
-  expect(builderMock).toHaveBeenCalled();
-});
-
-function shallowRender() {
-  return shallow(<CreateFormShim onClose={jest.fn()} onCreate={jest.fn()} />);
-}
index 4af5a8afb9f01ce31ab7ea92b0aa9fee7179091a..9e61241f479bfa266a9b2c443bd13abae2dd76aa 100644 (file)
@@ -255,7 +255,6 @@ export class AllProjects extends React.PureComponent<Props, State> {
         <div className="layout-page-main-inner">
           <PageHeader
             currentUser={this.props.currentUser}
-            isFavorite={this.props.isFavorite}
             loading={this.state.loading}
             onPerspectiveChange={this.handlePerspectiveChange}
             onQueryChange={this.updateLocationQuery}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx b/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx
new file mode 100644 (file)
index 0000000..12f4257
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { Button } from 'sonar-ui-common/components/controls/buttons';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getComponentNavigation } from '../../../api/nav';
+import CreateApplicationForm from '../../../app/components/extensions/CreateApplicationForm';
+import { withAppState } from '../../../components/hoc/withAppState';
+import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
+import { Router, withRouter } from '../../../components/hoc/withRouter';
+import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls';
+import { hasGlobalPermission } from '../../../helpers/users';
+import { ComponentQualifier } from '../../../types/component';
+
+export interface ApplicationCreationProps {
+  appState: Pick<T.AppState, 'qualifiers'>;
+  className?: string;
+  currentUser: T.LoggedInUser;
+  router: Router;
+}
+
+export function ApplicationCreation(props: ApplicationCreationProps) {
+  const { appState, className, currentUser, router } = props;
+
+  const [showForm, setShowForm] = React.useState(false);
+
+  const canCreateApplication =
+    appState.qualifiers.includes(ComponentQualifier.Application) &&
+    hasGlobalPermission(currentUser, 'applicationcreator');
+
+  if (!canCreateApplication) {
+    return null;
+  }
+
+  const handleComponentCreate = ({
+    key,
+    qualifier
+  }: {
+    key: string;
+    qualifier: ComponentQualifier;
+  }) => {
+    return getComponentNavigation({ component: key }).then(({ configuration }) => {
+      if (configuration && configuration.showSettings) {
+        router.push(getComponentAdminUrl(key, qualifier));
+      } else {
+        router.push(getComponentOverviewUrl(key, qualifier));
+      }
+      setShowForm(false);
+    });
+  };
+
+  return (
+    <div className={className}>
+      <Button className="button-primary" onClick={() => setShowForm(true)}>
+        {translate('projects.create_application')}
+      </Button>
+
+      {showForm && (
+        <CreateApplicationForm
+          onClose={() => setShowForm(false)}
+          onCreate={handleComponentCreate}
+        />
+      )}
+    </div>
+  );
+}
+
+export default withAppState(withCurrentUser(withRouter(ApplicationCreation)));
index 782fcfe3f7e65d7114757a7663271fc286377cb1..d4ed964fefe869777074fec1840246ed42d619ba 100644 (file)
@@ -48,7 +48,7 @@ export default class FavoriteFilter extends React.PureComponent<Props> {
 
     return (
       <header className="page-header text-center">
-        <div className="button-group">
+        <div className="button-group little-spacer-top">
           <Link
             activeClassName="button-active"
             className="button"
index 2a780820933a307f7f2c1c7c495e2310e3112f0e..d0fb81c5b52b750f1f61ec1b9b37d54de2a21b6d 100644 (file)
@@ -22,16 +22,16 @@ import * as React from 'react';
 import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import HomePageSelect from '../../../components/controls/HomePageSelect';
-import { isSonarCloud } from '../../../helpers/system';
 import { isLoggedIn } from '../../../helpers/users';
 import SearchFilterContainer from '../filters/SearchFilterContainer';
 import { Project } from '../types';
+import ApplicationCreation from './ApplicationCreation';
 import PerspectiveSelect from './PerspectiveSelect';
+import ProjectCreationMenu from './ProjectCreationMenu';
 import ProjectsSortingSelect from './ProjectsSortingSelect';
 
 interface Props {
   currentUser: T.CurrentUser;
-  isFavorite: boolean;
   loading: boolean;
   onPerspectiveChange: (x: { view: string; visualization?: string }) => void;
   onQueryChange: (change: T.RawQuery) => void;
@@ -48,58 +48,55 @@ export default function PageHeader(props: Props) {
   const { loading, total, projects, currentUser, view } = props;
   const limitReached = projects != null && total != null && projects.length < total;
   const defaultOption = isLoggedIn(currentUser) ? 'name' : 'analysis_date';
-  const showHomePageSelect = !isSonarCloud() || props.isFavorite;
 
-  return (
-    <header className="page-header projects-topbar-items">
-      <PerspectiveSelect
-        className="projects-topbar-item js-projects-perspective-select"
-        onChange={props.onPerspectiveChange}
-        view={props.view}
-        visualization={props.visualization}
-      />
+  const sortingDisabled = view === 'visualizations' && !limitReached;
 
-      {view === 'visualizations' && !limitReached ? (
-        <Tooltip overlay={translate('projects.sort.disabled')}>
-          <div className="projects-topbar-item disabled">
-            <ProjectsSortingSelect
-              className="js-projects-sorting-select"
-              defaultOption={defaultOption}
-              onChange={props.onSortChange}
-              selectedSort={props.selectedSort}
-              view={props.view}
-            />
-          </div>
-        </Tooltip>
-      ) : (
-        <ProjectsSortingSelect
-          className="projects-topbar-item js-projects-sorting-select"
-          defaultOption={defaultOption}
-          onChange={props.onSortChange}
-          selectedSort={props.selectedSort}
-          view={props.view}
-        />
-      )}
+  return (
+    <header className="page-header">
+      <div className="display-flex-space-between spacer-top">
+        <SearchFilterContainer onQueryChange={props.onQueryChange} query={props.query} />
+        <div className="display-flex-center">
+          <ProjectCreationMenu className="little-spacer-right" />
+          <ApplicationCreation className="little-spacer-right" />
+          <HomePageSelect
+            className="spacer-left little-spacer-right"
+            currentPage={{ type: 'PROJECTS' }}
+          />
+        </div>
+      </div>
+      <div className="big-spacer-top display-flex-space-between">
+        <div
+          className={classNames('display-flex-center', {
+            'is-loading': loading
+          })}>
+          {total != null && (
+            <span className="projects-total-label">
+              <strong id="projects-total">{total}</strong> {translate('projects._projects')}
+            </span>
+          )}
+        </div>
 
-      <SearchFilterContainer onQueryChange={props.onQueryChange} query={props.query} />
+        <div className="display-flex-center">
+          <PerspectiveSelect
+            className="projects-topbar-item js-projects-perspective-select"
+            onChange={props.onPerspectiveChange}
+            view={props.view}
+            visualization={props.visualization}
+          />
 
-      <div
-        className={classNames('projects-topbar-item', 'is-last', {
-          'is-loading': loading
-        })}>
-        {total != null && (
-          <span>
-            <strong id="projects-total">{total}</strong> {translate('projects._projects')}
-          </span>
-        )}
+          <Tooltip overlay={sortingDisabled ? translate('projects.sort.disabled') : undefined}>
+            <div className={classNames('projects-topbar-item', { disabled: sortingDisabled })}>
+              <ProjectsSortingSelect
+                className="js-projects-sorting-select"
+                defaultOption={defaultOption}
+                onChange={props.onSortChange}
+                selectedSort={props.selectedSort}
+                view={props.view}
+              />
+            </div>
+          </Tooltip>
+        </div>
       </div>
-
-      {showHomePageSelect && (
-        <HomePageSelect
-          className="huge-spacer-left"
-          currentPage={isSonarCloud() ? { type: 'MY_PROJECTS' } : { type: 'PROJECTS' }}
-        />
-      )}
     </header>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
new file mode 100644 (file)
index 0000000..9ead04d
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { Button } from 'sonar-ui-common/components/controls/buttons';
+import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
+import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getAlmSettings } from '../../../api/alm-settings';
+import { withAppState } from '../../../components/hoc/withAppState';
+import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
+import { hasGlobalPermission } from '../../../helpers/users';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import ProjectCreationMenuItem from './ProjectCreationMenuItem';
+
+interface Props {
+  appState: Pick<T.AppState, 'branchesEnabled'>;
+  className?: string;
+  currentUser: T.LoggedInUser;
+}
+
+interface State {
+  boundAlms: Array<string>;
+}
+
+const PROJECT_CREATION_PERMISSION = 'provisioning';
+/*
+ * ALMs for which the import feature has been implemented
+ */
+const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Azure, AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];
+
+const almSettingsValidators = {
+  [AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url,
+  [AlmKeys.Bitbucket]: (_: AlmSettingsInstance) => true,
+  [AlmKeys.GitHub]: (_: AlmSettingsInstance) => true,
+  [AlmKeys.GitLab]: (settings: AlmSettingsInstance) => !!settings.url
+};
+
+export class ProjectCreationMenu extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { boundAlms: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+
+    this.fetchAlmBindings();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  almSettingIsValid = (settings: AlmSettingsInstance) => {
+    return almSettingsValidators[settings.alm](settings);
+  };
+
+  fetchAlmBindings = async () => {
+    const {
+      appState: { branchesEnabled },
+      currentUser
+    } = this.props;
+    const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION);
+
+    // getAlmSettings requires branchesEnabled
+    if (!canCreateProject || !branchesEnabled) {
+      return;
+    }
+
+    const almSettings = await getAlmSettings();
+
+    // Import is only available if exactly one binding is configured
+    const boundAlms = IMPORT_COMPATIBLE_ALMS.filter(key => {
+      const currentAlmSettings = almSettings.filter(s => s.alm === key);
+      return currentAlmSettings.length === 1 && this.almSettingIsValid(currentAlmSettings[0]);
+    });
+
+    if (this.mounted) {
+      this.setState({
+        boundAlms
+      });
+    }
+  };
+
+  render() {
+    const { className, currentUser } = this.props;
+    const { boundAlms } = this.state;
+
+    const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION);
+
+    if (!canCreateProject) {
+      return null;
+    }
+
+    return (
+      <Dropdown
+        className={className}
+        onOpen={this.fetchAlmBindings}
+        overlay={
+          <ul className="menu">
+            {[...boundAlms, 'manual'].map(alm => (
+              <li key={alm}>
+                <ProjectCreationMenuItem alm={alm} />
+              </li>
+            ))}
+          </ul>
+        }>
+        <Button className="button-primary">
+          {translate('projects.add')}
+          <DropdownIcon className="spacer-left " />
+        </Button>
+      </Dropdown>
+    );
+  }
+}
+
+export default withAppState(withCurrentUser(ProjectCreationMenu));
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx
new file mode 100644 (file)
index 0000000..daf435d
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 ChevronsIcon from 'sonar-ui-common/components/icons/ChevronsIcon';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+
+export interface ProjectCreationMenuItemProps {
+  alm: string;
+}
+
+export default function ProjectCreationMenuItem(props: ProjectCreationMenuItemProps) {
+  const { alm } = props;
+  return (
+    <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>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx
new file mode 100644 (file)
index 0000000..fa411d0
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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, ShallowWrapper } from 'enzyme';
+import * as React from 'react';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import { getComponentNavigation } from '../../../../api/nav';
+import CreateApplicationForm from '../../../../app/components/extensions/CreateApplicationForm';
+import { mockAppState, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../types/component';
+import { ApplicationCreation, ApplicationCreationProps } from '../ApplicationCreation';
+
+jest.mock('../../../../api/nav', () => ({
+  getComponentNavigation: jest.fn().mockResolvedValue({})
+}));
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('available');
+  expect(
+    shallowRender({ appState: mockAppState({ qualifiers: [ComponentQualifier.Portfolio] }) })
+  ).toMatchSnapshot('unavailable');
+  expect(
+    shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: ['otherrights'] } }) })
+  ).toMatchSnapshot('not allowed');
+});
+
+it('should show form and callback when submitted - admin', async () => {
+  (getComponentNavigation as jest.Mock).mockResolvedValueOnce({
+    configuration: { showSettings: true }
+  });
+  const routerPush = jest.fn();
+  const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });
+
+  await openAndSubmitForm(wrapper);
+
+  expect(routerPush).toBeCalledWith({
+    pathname: '/application/console',
+    query: {
+      id: 'new app'
+    }
+  });
+});
+
+it('should show form and callback when submitted - user', async () => {
+  (getComponentNavigation as jest.Mock).mockResolvedValueOnce({
+    configuration: { showSettings: false }
+  });
+  const routerPush = jest.fn();
+  const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });
+
+  await openAndSubmitForm(wrapper);
+
+  expect(routerPush).toBeCalledWith({
+    pathname: '/dashboard',
+    query: {
+      id: 'new app'
+    }
+  });
+});
+
+async function openAndSubmitForm(wrapper: ShallowWrapper) {
+  wrapper.find(Button).simulate('click');
+
+  const creationForm = wrapper.find(CreateApplicationForm);
+  expect(creationForm.exists()).toBe(true);
+
+  await creationForm
+    .props()
+    .onCreate({ key: 'new app', qualifier: ComponentQualifier.Application });
+  expect(getComponentNavigation).toBeCalled();
+  expect(wrapper.find(CreateApplicationForm).exists()).toBe(false);
+}
+
+function shallowRender(overrides: Partial<ApplicationCreationProps> = {}) {
+  return shallow(
+    <ApplicationCreation
+      appState={mockAppState({ qualifiers: [ComponentQualifier.Application] })}
+      currentUser={mockLoggedInUser({ permissions: { global: ['applicationcreator'] } })}
+      router={mockRouter()}
+      {...overrides}
+    />
+  );
+}
index 9997b42ba97242f819b20cf0be89729366ecd774..2565eb64c1ded36eda0db7eb4643cf24de1ca0e3 100644 (file)
@@ -71,7 +71,6 @@ function shallowRender(props?: {}) {
   return shallow(
     <PageHeader
       currentUser={{ isLoggedIn: false }}
-      isFavorite={false}
       loading={false}
       onPerspectiveChange={jest.fn()}
       onQueryChange={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx
new file mode 100644 (file)
index 0000000..51deead
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { getAlmSettings } from '../../../../api/alm-settings';
+import { mockAppState, mockLoggedInUser } from '../../../../helpers/testMocks';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ProjectCreationMenu } from '../ProjectCreationMenu';
+
+jest.mock('../../../../api/alm-settings', () => ({
+  getAlmSettings: jest.fn().mockResolvedValue([])
+}));
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(
+    shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) })
+  ).toMatchSnapshot('not allowed');
+});
+
+it('should fetch alm bindings on mount', async () => {
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(getAlmSettings).toBeCalled();
+});
+
+it('should not fetch alm bindings if user cannot create projects', async () => {
+  const wrapper = shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) });
+  await waitAndUpdate(wrapper);
+  expect(getAlmSettings).not.toBeCalled();
+});
+
+it('should not fetch alm bindings if branches are not enabled', async () => {
+  const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: false }) });
+  await waitAndUpdate(wrapper);
+  expect(getAlmSettings).not.toBeCalled();
+});
+
+it('should filter alm bindings appropriately', async () => {
+  (getAlmSettings as jest.Mock).mockResolvedValueOnce([
+    { alm: AlmKeys.Azure },
+    { alm: AlmKeys.Bitbucket, url: 'b1' },
+    { alm: AlmKeys.Bitbucket, url: 'b2' },
+    { alm: AlmKeys.GitHub },
+    { alm: AlmKeys.GitLab, url: 'gitlab.com' }
+  ]);
+
+  const wrapper = shallowRender();
+
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.state().boundAlms).toEqual([AlmKeys.GitHub, AlmKeys.GitLab]);
+});
+
+function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) {
+  return shallow<ProjectCreationMenu>(
+    <ProjectCreationMenu
+      appState={mockAppState({ branchesEnabled: true })}
+      currentUser={mockLoggedInUser({ permissions: { global: ['provisioning'] } })}
+      {...overrides}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx
new file mode 100644 (file)
index 0000000..6532542
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { AlmKeys } from '../../../../types/alm-settings';
+import ProjectCreationMenuItem, { ProjectCreationMenuItemProps } from '../ProjectCreationMenuItem';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('bitbucket');
+  expect(shallowRender({ alm: 'manual' })).toMatchSnapshot('manual');
+});
+
+function shallowRender(overrides: Partial<ProjectCreationMenuItemProps> = {}) {
+  return shallow(<ProjectCreationMenuItem alm={AlmKeys.Bitbucket} {...overrides} />);
+}
index 3fe004cebcf568920aa943c284ff9356c87d5e7f..a4497f47c20f05833a6dffa9492cadc353e399b3 100644 (file)
@@ -66,7 +66,6 @@ exports[`renders 1`] = `
                 "isLoggedIn": true,
               }
             }
-            isFavorite={false}
             loading={false}
             onPerspectiveChange={[Function]}
             onQueryChange={[Function]}
@@ -220,7 +219,6 @@ exports[`renders 2`] = `
                 "isLoggedIn": true,
               }
             }
-            isFavorite={false}
             loading={false}
             onPerspectiveChange={[Function]}
             onQueryChange={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap
new file mode 100644 (file)
index 0000000..9647b1e
--- /dev/null
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: available 1`] = `
+<div>
+  <Button
+    className="button-primary"
+    onClick={[Function]}
+  >
+    projects.create_application
+  </Button>
+</div>
+`;
+
+exports[`should render correctly: not allowed 1`] = `""`;
+
+exports[`should render correctly: unavailable 1`] = `""`;
index 7be7ce5eb93ba5c80db727108cdfd9b421046949..4793b31af1954ac2f9ea0166d63d7e5a3b7f7431 100644 (file)
@@ -5,7 +5,7 @@ exports[`renders for logged in user 1`] = `
   className="page-header text-center"
 >
   <div
-    className="button-group"
+    className="button-group little-spacer-top"
   >
     <Link
       activeClassName="button-active"
index 057c60483688276b27220b09169ebcc7f31362cc..2b6933ea8724c28744f4978a0f7aa784239d3fbc 100644 (file)
 
 exports[`should render correctly 1`] = `
 <header
-  className="page-header projects-topbar-items"
+  className="page-header"
 >
-  <PerspectiveSelect
-    className="projects-topbar-item js-projects-perspective-select"
-    onChange={[MockFunction]}
-    view="overall"
-  />
-  <ProjectsSortingSelect
-    className="projects-topbar-item js-projects-sorting-select"
-    defaultOption="analysis_date"
-    onChange={[MockFunction]}
-    selectedSort="size"
-    view="overall"
-  />
-  <SearchFilterContainer
-    onQueryChange={[MockFunction]}
-    query={
-      Object {
-        "search": "test",
+  <div
+    className="display-flex-space-between spacer-top"
+  >
+    <SearchFilterContainer
+      onQueryChange={[MockFunction]}
+      query={
+        Object {
+          "search": "test",
+        }
       }
-    }
-  />
+    />
+    <div
+      className="display-flex-center"
+    >
+      <Connect(withAppState(Connect(withCurrentUser(ProjectCreationMenu))))
+        className="little-spacer-right"
+      />
+      <Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation)))))
+        className="little-spacer-right"
+      />
+      <Connect(HomePageSelect)
+        className="spacer-left little-spacer-right"
+        currentPage={
+          Object {
+            "type": "PROJECTS",
+          }
+        }
+      />
+    </div>
+  </div>
   <div
-    className="projects-topbar-item is-last"
+    className="big-spacer-top display-flex-space-between"
   >
-    <span>
-      <strong
-        id="projects-total"
+    <div
+      className="display-flex-center"
+    >
+      <span
+        className="projects-total-label"
       >
-        12
-      </strong>
-       
-      projects._projects
-    </span>
+        <strong
+          id="projects-total"
+        >
+          12
+        </strong>
+         
+        projects._projects
+      </span>
+    </div>
+    <div
+      className="display-flex-center"
+    >
+      <PerspectiveSelect
+        className="projects-topbar-item js-projects-perspective-select"
+        onChange={[MockFunction]}
+        view="overall"
+      />
+      <Tooltip>
+        <div
+          className="projects-topbar-item"
+        >
+          <ProjectsSortingSelect
+            className="js-projects-sorting-select"
+            defaultOption="analysis_date"
+            onChange={[MockFunction]}
+            selectedSort="size"
+            view="overall"
+          />
+        </div>
+      </Tooltip>
+    </div>
   </div>
-  <Connect(HomePageSelect)
-    className="huge-spacer-left"
-    currentPage={
-      Object {
-        "type": "PROJECTS",
-      }
-    }
-  />
 </header>
 `;
 
 exports[`should render correctly while loading 1`] = `
 <header
-  className="page-header projects-topbar-items"
+  className="page-header"
 >
-  <PerspectiveSelect
-    className="projects-topbar-item js-projects-perspective-select"
-    onChange={[MockFunction]}
-    view="overall"
-  />
-  <ProjectsSortingSelect
-    className="projects-topbar-item js-projects-sorting-select"
-    defaultOption="analysis_date"
-    onChange={[MockFunction]}
-    selectedSort="size"
-    view="overall"
-  />
-  <SearchFilterContainer
-    onQueryChange={[MockFunction]}
-    query={
-      Object {
-        "search": "test",
+  <div
+    className="display-flex-space-between spacer-top"
+  >
+    <SearchFilterContainer
+      onQueryChange={[MockFunction]}
+      query={
+        Object {
+          "search": "test",
+        }
       }
-    }
-  />
+    />
+    <div
+      className="display-flex-center"
+    >
+      <Connect(withAppState(Connect(withCurrentUser(ProjectCreationMenu))))
+        className="little-spacer-right"
+      />
+      <Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation)))))
+        className="little-spacer-right"
+      />
+      <Connect(HomePageSelect)
+        className="spacer-left little-spacer-right"
+        currentPage={
+          Object {
+            "type": "PROJECTS",
+          }
+        }
+      />
+    </div>
+  </div>
   <div
-    className="projects-topbar-item is-last is-loading"
+    className="big-spacer-top display-flex-space-between"
   >
-    <span>
-      <strong
-        id="projects-total"
+    <div
+      className="display-flex-center is-loading"
+    >
+      <span
+        className="projects-total-label"
       >
-        2
-      </strong>
-       
-      projects._projects
-    </span>
+        <strong
+          id="projects-total"
+        >
+          2
+        </strong>
+         
+        projects._projects
+      </span>
+    </div>
+    <div
+      className="display-flex-center"
+    >
+      <PerspectiveSelect
+        className="projects-topbar-item js-projects-perspective-select"
+        onChange={[MockFunction]}
+        view="overall"
+      />
+      <Tooltip>
+        <div
+          className="projects-topbar-item"
+        >
+          <ProjectsSortingSelect
+            className="js-projects-sorting-select"
+            defaultOption="analysis_date"
+            onChange={[MockFunction]}
+            selectedSort="size"
+            view="overall"
+          />
+        </div>
+      </Tooltip>
+    </div>
   </div>
-  <Connect(HomePageSelect)
-    className="huge-spacer-left"
-    currentPage={
-      Object {
-        "type": "PROJECTS",
-      }
-    }
-  />
 </header>
 `;
 
 exports[`should render disabled sorting options for visualizations 1`] = `
 <header
-  className="page-header projects-topbar-items"
+  className="page-header"
 >
-  <PerspectiveSelect
-    className="projects-topbar-item js-projects-perspective-select"
-    onChange={[MockFunction]}
-    view="visualizations"
-    visualization="coverage"
-  />
-  <Tooltip
-    overlay="projects.sort.disabled"
+  <div
+    className="display-flex-space-between spacer-top"
+  >
+    <SearchFilterContainer
+      onQueryChange={[MockFunction]}
+      query={
+        Object {
+          "search": "test",
+        }
+      }
+    />
+    <div
+      className="display-flex-center"
+    >
+      <Connect(withAppState(Connect(withCurrentUser(ProjectCreationMenu))))
+        className="little-spacer-right"
+      />
+      <Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation)))))
+        className="little-spacer-right"
+      />
+      <Connect(HomePageSelect)
+        className="spacer-left little-spacer-right"
+        currentPage={
+          Object {
+            "type": "PROJECTS",
+          }
+        }
+      />
+    </div>
+  </div>
+  <div
+    className="big-spacer-top display-flex-space-between"
   >
     <div
-      className="projects-topbar-item disabled"
+      className="display-flex-center"
+    />
+    <div
+      className="display-flex-center"
     >
-      <ProjectsSortingSelect
-        className="js-projects-sorting-select"
-        defaultOption="analysis_date"
+      <PerspectiveSelect
+        className="projects-topbar-item js-projects-perspective-select"
         onChange={[MockFunction]}
-        selectedSort="size"
         view="visualizations"
+        visualization="coverage"
       />
+      <Tooltip
+        overlay="projects.sort.disabled"
+      >
+        <div
+          className="projects-topbar-item disabled"
+        >
+          <ProjectsSortingSelect
+            className="js-projects-sorting-select"
+            defaultOption="analysis_date"
+            onChange={[MockFunction]}
+            selectedSort="size"
+            view="visualizations"
+          />
+        </div>
+      </Tooltip>
     </div>
-  </Tooltip>
-  <SearchFilterContainer
-    onQueryChange={[MockFunction]}
-    query={
-      Object {
-        "search": "test",
-      }
-    }
-  />
-  <div
-    className="projects-topbar-item is-last"
-  />
-  <Connect(HomePageSelect)
-    className="huge-spacer-left"
-    currentPage={
-      Object {
-        "type": "PROJECTS",
-      }
-    }
-  />
+  </div>
 </header>
 `;
 
 exports[`should render switch the default sorting option for anonymous users 1`] = `
 <ProjectsSortingSelect
-  className="projects-topbar-item js-projects-sorting-select"
+  className="js-projects-sorting-select"
   defaultOption="name"
   onChange={[MockFunction]}
   selectedSort="size"
@@ -155,7 +237,7 @@ exports[`should render switch the default sorting option for anonymous users 1`]
 
 exports[`should render switch the default sorting option for anonymous users 2`] = `
 <ProjectsSortingSelect
-  className="projects-topbar-item js-projects-sorting-select"
+  className="js-projects-sorting-select"
   defaultOption="analysis_date"
   onChange={[MockFunction]}
   selectedSort="size"
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap
new file mode 100644 (file)
index 0000000..0316a62
--- /dev/null
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<Dropdown
+  onOpen={[Function]}
+  overlay={
+    <ul
+      className="menu"
+    >
+      <li>
+        <ProjectCreationMenuItem
+          alm="manual"
+        />
+      </li>
+    </ul>
+  }
+>
+  <Button
+    className="button-primary"
+  >
+    projects.add
+    <DropdownIcon
+      className="spacer-left "
+    />
+  </Button>
+</Dropdown>
+`;
+
+exports[`should render correctly: not allowed 1`] = `""`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap
new file mode 100644 (file)
index 0000000..e66677e
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: bitbucket 1`] = `
+<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>
+`;
+
+exports[`should render correctly: manual 1`] = `
+<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>
+`;
index a126e8a60792463570067c550a662491c1c69d4c..e358a8f27682618dde4f6e2824ea251eb2862875 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.
  */
-.projects-topbar-items {
-  display: flex;
-  align-items: center;
-  flex-grow: 1;
+.projects-page .layout-page-header-panel-inner,
+.projects-page .layout-page-header-panel {
+  height: 98px;
+  line-height: normal;
 }
 
 .projects-topbar-item + .projects-topbar-item {
index 847d365e90cf736a1622f0a5b89dd8cbca3ef87a..9ccf4f1a64ebde4b9de05f1368b836ec69304b03 100644 (file)
@@ -938,6 +938,8 @@ issue_bulk_change.no_match=There is no issue matching your filter selection
 
 projects.page=Projects
 projects._projects=projects
+projects.add=Add project
+projects.create_application=Create Application
 projects.no_projects.empty_instance=There are no visible projects yet.
 projects.no_projects.empty_instance.new_project=Once you analyze some projects, they will show up here.
 projects.no_projects.empty_instance.how_to_add_projects=Here is how you can analyze new projects