*/
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';
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;
<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>
+++ /dev/null
-/*
- * 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);
+++ /dev/null
-/*
- * 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>
- );
-}
+++ /dev/null
-/*
- * 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()}
- />
- );
-}
+++ /dev/null
-/*
- * 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}
- />
- );
-}
}
}
/>
- <GlobalNavPlus
- appState={
- Object {
- "canAdmin": false,
- "globalPages": Array [],
- "qualifiers": Array [],
- }
- }
- currentUser={
- Object {
- "isLoggedIn": true,
- }
- }
- />
<withRouter(GlobalNavUser)
currentUser={
Object {
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-// 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>
-`;
+++ /dev/null
-/*
- * 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()
- });
- }
-}
+++ /dev/null
-/*
- * 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()} />);
-}
<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}
--- /dev/null
+/*
+ * 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)));
return (
<header className="page-header text-center">
- <div className="button-group">
+ <div className="button-group little-spacer-top">
<Link
activeClassName="button-active"
className="button"
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;
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>
);
}
--- /dev/null
+/*
+ * 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));
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
return shallow(
<PageHeader
currentUser={{ isLoggedIn: false }}
- isFavorite={false}
loading={false}
onPerspectiveChange={jest.fn()}
onQueryChange={jest.fn()}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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} />);
+}
"isLoggedIn": true,
}
}
- isFavorite={false}
loading={false}
onPerspectiveChange={[Function]}
onQueryChange={[Function]}
"isLoggedIn": true,
}
}
- isFavorite={false}
loading={false}
onPerspectiveChange={[Function]}
onQueryChange={[Function]}
--- /dev/null
+// 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`] = `""`;
className="page-header text-center"
>
<div
- className="button-group"
+ className="button-group little-spacer-top"
>
<Link
activeClassName="button-active"
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"
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"
--- /dev/null
+// 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`] = `""`;
--- /dev/null
+// 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>
+`;
* 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 {
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