aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2020-06-04 09:35:00 +0200
committersonartech <sonartech@sonarsource.com>2020-07-01 20:05:53 +0000
commit0f5ec7677c8ce61d568b78ead47a6d1279f796ba (patch)
tree64fa94ff817afd29e2f63a69400a5bbc9ce281b5 /server
parent39d0d57a4979ba62e8b1c5effb97033113d3daaa (diff)
downloadsonarqube-0f5ec7677c8ce61d568b78ead47a6d1279f796ba.tar.gz
sonarqube-0f5ec7677c8ce61d568b78ead47a6d1279f796ba.zip
SONAR-13479 new creation menu
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx158
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx99
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx141
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx76
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap141
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap224
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/menu.css11
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css5
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/types.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx3
13 files changed, 603 insertions, 263 deletions
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
index 2d3f41d9629..f94ebbb8574 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
@@ -18,33 +18,46 @@
* 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
index 00000000000..b3891faebb5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
index 4914ea8d02a..62efab3775a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
@@ -17,88 +17,105 @@
* 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
index 00000000000..4eb0f379c14
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx
@@ -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}
+ />
+ );
+}
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
index 11427944e71..658c06d2ab9 100644
--- 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
@@ -1,112 +1,57 @@
// 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
index 00000000000..e10a9f2ac36
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css
index 2062096a1de..64d39525208 100644
--- a/server/sonar-web/src/main/js/app/styles/components/menu.css
+++ b/server/sonar-web/src/main/js/app/styles/components/menu.css
@@ -37,6 +37,7 @@
.menu-item,
.menu > li > a,
+.menu > li > button,
.menu > li > span {
display: block;
padding: 4px 16px;
@@ -54,6 +55,12 @@
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);
diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css
index b05d35cd039..b987a065653 100644
--- a/server/sonar-web/src/main/js/app/styles/init/misc.css
+++ b/server/sonar-web/src/main/js/app/styles/init/misc.css
@@ -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;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap
index 6ba025c784e..fa7fa8fc11d 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap
@@ -68,7 +68,7 @@ exports[`should render correctly: no projects 1`] = `
Object {
"pathname": "/projects/create",
"query": Object {
- "mode": "bbs",
+ "mode": "bitbucket",
"resetPat": 1,
},
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap
index ced072128a2..03b1ab6bdf0 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap
@@ -263,7 +263,7 @@ exports[`should render correctly: no repos 1`] = `
Object {
"pathname": "/projects/create",
"query": Object {
- "mode": "bbs",
+ "mode": "bitbucket",
"resetPat": 1,
},
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
index 05531913bb2..99b42043c24 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
@@ -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 {},
diff --git a/server/sonar-web/src/main/js/apps/create/project/types.ts b/server/sonar-web/src/main/js/apps/create/project/types.ts
index b316a4e781a..d991cd60d3a 100644
--- a/server/sonar-web/src/main/js/apps/create/project/types.ts
+++ b/server/sonar-web/src/main/js/apps/create/project/types.ts
@@ -19,5 +19,5 @@
*/
export enum CreateProjectModes {
Manual = 'manual',
- BitbucketServer = 'bbs'
+ BitbucketServer = 'bitbucket'
}
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
index 7dd89ec8cc2..96f9ab98e86 100644
--- a/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx
@@ -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> {