Просмотр исходного кода

SONAR-13887 Move creation menu

tags/8.7.0.41497
Jeremy Davis 3 лет назад
Родитель
Сommit
0592cda7b6
26 измененных файлов: 770 добавлений и 1059 удалений
  1. 0
    5
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
  2. 0
    193
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  3. 0
    99
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx
  4. 0
    169
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
  5. 0
    76
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx
  6. 0
    14
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
  7. 0
    83
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
  8. 0
    224
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap
  9. 0
    1
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
  10. 85
    0
      server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx
  11. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
  12. 46
    49
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
  13. 132
    0
      server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
  14. 25
    17
      server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx
  15. 99
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx
  16. 0
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
  17. 85
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx
  18. 7
    10
      server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx
  19. 0
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
  20. 16
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap
  21. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/FavoriteFilter-test.tsx.snap
  22. 192
    110
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
  23. 29
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap
  24. 46
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap
  25. 4
    4
      server/sonar-web/src/main/js/apps/projects/styles.css
  26. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 5
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx Просмотреть файл

@@ -19,9 +19,7 @@
*/
import * as React from 'react';
import { connect } from 'react-redux';
import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';
import NavBar from 'sonar-ui-common/components/ui/NavBar';
import { isLoggedIn } from '../../../../helpers/users';
import { getAppState, getCurrentUser, Store } from '../../../../store/rootReducer';
import { rawSizes } from '../../../theme';
import EmbedDocsPopupHelper from '../../embed-docs-modal/EmbedDocsPopupHelper';
@@ -31,8 +29,6 @@ import GlobalNavBranding from './GlobalNavBranding';
import GlobalNavMenu from './GlobalNavMenu';
import GlobalNavUser from './GlobalNavUser';

const GlobalNavPlus = lazyLoadComponent(() => import('./GlobalNavPlus'), 'GlobalNavPlus');

export interface GlobalNavProps {
appState: Pick<T.AppState, 'canAdmin' | 'globalPages' | 'qualifiers'>;
currentUser: T.CurrentUser;
@@ -50,7 +46,6 @@ export function GlobalNav(props: GlobalNavProps) {
<ul className="global-navbar-menu global-navbar-menu-right">
<EmbedDocsPopupHelper />
<Search currentUser={currentUser} />
{isLoggedIn(currentUser) && <GlobalNavPlus appState={appState} currentUser={currentUser} />}
<GlobalNavUser currentUser={currentUser} />
</ul>
</NavBar>

+ 0
- 193
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx Просмотреть файл

@@ -1,193 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
import PlusIcon from 'sonar-ui-common/components/icons/PlusIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getAlmSettings } from '../../../../api/alm-settings';
import { getComponentNavigation } from '../../../../api/nav';
import CreateFormShim from '../../../../apps/portfolio/components/CreateFormShim';
import { Router, withRouter } from '../../../../components/hoc/withRouter';
import { getExtensionStart } from '../../../../helpers/extensions';
import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../../helpers/urls';
import { hasGlobalPermission } from '../../../../helpers/users';
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../types/component';
import CreateApplicationForm from '../../extensions/CreateApplicationForm';
import GlobalNavPlusMenu from './GlobalNavPlusMenu';

interface Props {
appState: Pick<T.AppState, 'branchesEnabled' | 'qualifiers'>;
currentUser: T.LoggedInUser;
router: Router;
}

interface State {
boundAlms: Array<string>;
creatingComponent?: ComponentQualifier;
governanceReady: boolean;
}

/*
* ALMs for which the import feature has been implemented
*/
const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Azure, AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];

const almSettingsValidators = {
[AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url,
[AlmKeys.Bitbucket]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitHub]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitLab]: (settings: AlmSettingsInstance) => !!settings.url
};

export class GlobalNavPlus extends React.PureComponent<Props, State> {
mounted = false;
state: State = { boundAlms: [], governanceReady: false };

componentDidMount() {
this.mounted = true;

this.fetchAlmBindings();

if (this.props.appState.qualifiers.includes(ComponentQualifier.Portfolio)) {
getExtensionStart('governance/console').then(
() => {
if (this.mounted) {
this.setState({ governanceReady: true });
}
},
() => {
/* error handled globally */
}
);
}
}

componentWillUnmount() {
this.mounted = false;
}

closeComponentCreationForm = () => {
this.setState({ creatingComponent: undefined });
};

almSettingIsValid = (settings: AlmSettingsInstance) => {
return almSettingsValidators[settings.alm](settings);
};

fetchAlmBindings = async () => {
const {
appState: { branchesEnabled },
currentUser
} = this.props;
const canCreateProject = hasGlobalPermission(currentUser, 'provisioning');

// getAlmSettings requires branchesEnabled
if (!canCreateProject || !branchesEnabled) {
return;
}

const almSettings = await getAlmSettings();

// Import is only available if exactly one binding is configured
const boundAlms = IMPORT_COMPATIBLE_ALMS.filter(key => {
const currentAlmSettings = almSettings.filter(s => s.alm === key);
return currentAlmSettings.length === 1 && this.almSettingIsValid(currentAlmSettings[0]);
});

if (this.mounted) {
this.setState({
boundAlms
});
}
};

handleComponentCreationClick = (qualifier: ComponentQualifier) => {
this.setState({ creatingComponent: qualifier });
};

handleComponentCreate = ({ key, qualifier }: { key: string; qualifier: ComponentQualifier }) => {
return getComponentNavigation({ component: key }).then(({ configuration }) => {
if (configuration && configuration.showSettings) {
this.props.router.push(getComponentAdminUrl(key, qualifier));
} else {
this.props.router.push(getComponentOverviewUrl(key, qualifier));
}
this.closeComponentCreationForm();
});
};

render() {
const { appState, currentUser } = this.props;
const { boundAlms, governanceReady, creatingComponent } = this.state;
const canCreateApplication =
appState.qualifiers.includes(ComponentQualifier.Application) &&
hasGlobalPermission(currentUser, 'applicationcreator');
const canCreatePortfolio =
appState.qualifiers.includes(ComponentQualifier.Portfolio) &&
hasGlobalPermission(currentUser, 'portfoliocreator');
const canCreateProject = hasGlobalPermission(currentUser, 'provisioning');

if (!canCreateProject && !canCreateApplication && !canCreatePortfolio) {
return null;
}

return (
<>
<Dropdown
onOpen={this.fetchAlmBindings}
overlay={
<GlobalNavPlusMenu
canCreateApplication={canCreateApplication}
canCreatePortfolio={canCreatePortfolio}
canCreateProject={canCreateProject}
compatibleAlms={boundAlms}
onComponentCreationClick={this.handleComponentCreationClick}
/>
}
tagName="li">
<a
className="navbar-icon navbar-plus"
href="#"
title={translate('my_account.create_new_project_portfolio_or_application')}>
<PlusIcon />
</a>
</Dropdown>

{canCreateApplication && creatingComponent === ComponentQualifier.Application && (
<CreateApplicationForm
onClose={this.closeComponentCreationForm}
onCreate={this.handleComponentCreate}
/>
)}

{governanceReady && creatingComponent === ComponentQualifier.Portfolio && (
<CreateFormShim
defaultQualifier={creatingComponent}
onClose={this.closeComponentCreationForm}
onCreate={this.handleComponentCreate}
/>
)}
</>
);
}
}

export default withRouter(GlobalNavPlus);

+ 0
- 99
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx Просмотреть файл

@@ -1,99 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Link } from 'react-router';
import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
import ChevronsIcon from 'sonar-ui-common/components/icons/ChevronsIcon';
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import { ComponentQualifier } from '../../../../types/component';

export interface GlobalNavPlusMenuProps {
canCreateApplication: boolean;
canCreatePortfolio: boolean;
canCreateProject: boolean;
compatibleAlms: Array<string>;
onComponentCreationClick: (componentQualifier: ComponentQualifier) => void;
}

function renderCreateProjectOptions(compatibleAlms: Array<string>) {
return [...compatibleAlms, 'manual'].map(alm => (
<li key={alm}>
<Link
className="display-flex-center"
to={{ pathname: '/projects/create', query: { mode: alm } }}>
{alm === 'manual' ? (
<ChevronsIcon className="spacer-right" />
) : (
<img
alt={alm}
className="spacer-right"
width={16}
src={`${getBaseUrl()}/images/alm/${alm}.svg`}
/>
)}
{translate('my_account.add_project', alm)}
</Link>
</li>
));
}

function renderCreateComponent(
componentQualifier: ComponentQualifier,
onClick: (qualifier: ComponentQualifier) => void
) {
return (
<li>
<ButtonLink
className="display-flex-justify-start padded-left"
onClick={() => onClick(componentQualifier)}>
<QualifierIcon className="spacer-right" qualifier={componentQualifier} />
{translate('my_account.create_new', componentQualifier)}
</ButtonLink>
</li>
);
}

export default function GlobalNavPlusMenu(props: GlobalNavPlusMenuProps) {
const { canCreateApplication, canCreatePortfolio, canCreateProject, compatibleAlms } = props;

return (
<ul className="menu">
{canCreateProject && (
<>
<li className="menu-header">
<strong>{translate('my_account.add_project')}</strong>
</li>
{renderCreateProjectOptions(compatibleAlms)}
</>
)}
{(canCreateApplication || canCreatePortfolio) && (
<>
{canCreateProject && <li className="divider" />}
{canCreatePortfolio &&
renderCreateComponent(ComponentQualifier.Portfolio, props.onComponentCreationClick)}
{canCreateApplication &&
renderCreateComponent(ComponentQualifier.Application, props.onComponentCreationClick)}
</>
)}
</ul>
);
}

+ 0
- 169
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx Просмотреть файл

@@ -1,169 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getAlmSettings } from '../../../../../api/alm-settings';
import { getComponentNavigation } from '../../../../../api/nav';
import CreateFormShim from '../../../../../apps/portfolio/components/CreateFormShim';
import { mockLoggedInUser, mockRouter } from '../../../../../helpers/testMocks';
import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../../../helpers/urls';
import { AlmKeys } from '../../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../../types/component';
import { GlobalNavPlus } from '../GlobalNavPlus';

const PROJECT_CREATION_RIGHT = 'provisioning';
const APP_CREATION_RIGHT = 'applicationcreator';
const PORTFOLIO_CREATION_RIGHT = 'portfoliocreator';

jest.mock('../../../../../helpers/extensions', () => ({
getExtensionStart: jest.fn().mockResolvedValue(null)
}));

jest.mock('../../../../../api/alm-settings', () => ({
getAlmSettings: jest.fn().mockResolvedValue([])
}));

jest.mock('../../../../../api/nav', () => ({
getComponentNavigation: jest.fn().mockResolvedValue({})
}));

jest.mock('../../../../../helpers/urls', () => ({
getComponentOverviewUrl: jest.fn(),
getComponentAdminUrl: jest.fn()
}));

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly when no rights', async () => {
const wrapper = shallowRender([], {});
expect(wrapper.type()).toBeNull();
await waitAndUpdate(wrapper);
expect(getAlmSettings).not.toBeCalled();
});

it('should render correctly if branches not enabled', async () => {
const wrapper = shallowRender([PROJECT_CREATION_RIGHT], { branchesEnabled: false });
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(getAlmSettings).not.toBeCalled();
});

it('should render correctly', async () => {
expect(
shallowRender([APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT], {})
).toMatchSnapshot('no governance');

const wrapper = shallowRender(
[APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT],
{ enableGovernance: true }
);
await waitAndUpdate(wrapper);
wrapper.setState({ boundAlms: ['bitbucket'] });
expect(wrapper).toMatchSnapshot('full rights and alms');
});

it('should load correctly', async () => {
(getAlmSettings as jest.Mock).mockResolvedValueOnce([
{ alm: AlmKeys.Azure, key: 'A1' }, // No azure onboarding for now
{ alm: AlmKeys.Bitbucket, key: 'B1' },
{ alm: AlmKeys.GitHub, key: 'GH1' },
{ alm: AlmKeys.GitLab, key: 'GL1', url: 'ok' }
]);

const wrapper = shallowRender([PROJECT_CREATION_RIGHT], {});

await waitAndUpdate(wrapper);

expect(getAlmSettings).toBeCalled();
expect(wrapper.state().boundAlms).toEqual([AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab]);
});

it('should load without gitlab when no url', async () => {
(getAlmSettings as jest.Mock).mockResolvedValueOnce([{ alm: AlmKeys.GitLab, key: 'GL1' }]);

const wrapper = shallowRender([PROJECT_CREATION_RIGHT], {});

await waitAndUpdate(wrapper);

expect(getAlmSettings).toBeCalled();
expect(wrapper.state().boundAlms).toEqual([]);
});

it('should display component creation form', () => {
const wrapper = shallowRender([PORTFOLIO_CREATION_RIGHT], { enableGovernance: true });

wrapper.instance().handleComponentCreationClick(ComponentQualifier.Portfolio);
wrapper.setState({ governanceReady: true });

expect(wrapper.find(CreateFormShim).exists()).toBe(true);
});

describe('handleComponentCreate', () => {
(getComponentNavigation as jest.Mock)
.mockResolvedValueOnce({
configuration: { showSettings: true }
})
.mockResolvedValueOnce({});

const portfolio = { key: 'portfolio', qualifier: ComponentQualifier.Portfolio };

const wrapper = shallowRender([], { enableGovernance: true });

it('should redirect to admin', async () => {
wrapper.instance().handleComponentCreate(portfolio);
await waitAndUpdate(wrapper);
expect(getComponentAdminUrl).toBeCalledWith(portfolio.key, portfolio.qualifier);
expect(wrapper.state().creatingComponent).toBeUndefined();
});

it('should redirect to dashboard', async () => {
wrapper.instance().handleComponentCreate(portfolio);
await waitAndUpdate(wrapper);

expect(getComponentOverviewUrl).toBeCalledWith(portfolio.key, portfolio.qualifier);
});
});

function shallowRender(
permissions: string[] = [],
{ enableGovernance = false, branchesEnabled = true }
) {
let qualifiers: ComponentQualifier[];
if (enableGovernance) {
qualifiers = [ComponentQualifier.Portfolio, ComponentQualifier.Application];
} else if (branchesEnabled) {
qualifiers = [ComponentQualifier.Application];
} else {
qualifiers = [];
}
return shallow<GlobalNavPlus>(
<GlobalNavPlus
appState={{
branchesEnabled,
qualifiers
}}
currentUser={mockLoggedInUser({ permissions: { global: permissions } })}
router={mockRouter()}
/>
);
}

+ 0
- 76
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx Просмотреть файл

@@ -1,76 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
import { AlmKeys } from '../../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../../types/component';
import GlobalNavPlusMenu, { GlobalNavPlusMenuProps } from '../GlobalNavPlusMenu';

it('should render correctly', () => {
expect(shallowRender({ canCreateApplication: true })).toMatchSnapshot('app only');
expect(shallowRender({ canCreatePortfolio: true })).toMatchSnapshot('portfolio only');
expect(shallowRender({ canCreateProject: true })).toMatchSnapshot('project only');
expect(
shallowRender({ canCreateProject: true, compatibleAlms: [AlmKeys.Bitbucket] })
).toMatchSnapshot('imports');
expect(
shallowRender({
canCreateApplication: true,
canCreatePortfolio: true,
canCreateProject: true,
compatibleAlms: [AlmKeys.Bitbucket]
})
).toMatchSnapshot('all');
});

it('should trigger onClick', () => {
const onComponentCreationClick = jest.fn();
const wrapper = shallowRender({
canCreateApplication: true,
canCreatePortfolio: true,
onComponentCreationClick
});

// Portfolio
const portfolioButton = wrapper.find(ButtonLink).at(0);
portfolioButton.simulate('click');
expect(onComponentCreationClick).toBeCalledWith(ComponentQualifier.Portfolio);

onComponentCreationClick.mockClear();

// App
const appButton = wrapper.find(ButtonLink).at(1);
appButton.simulate('click');
expect(onComponentCreationClick).toBeCalledWith(ComponentQualifier.Application);
});

function shallowRender(overrides: Partial<GlobalNavPlusMenuProps> = {}) {
return shallow(
<GlobalNavPlusMenu
canCreateApplication={false}
canCreatePortfolio={false}
canCreateProject={false}
compatibleAlms={[]}
onComponentCreationClick={jest.fn()}
{...overrides}
/>
);
}

+ 0
- 14
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap Просмотреть файл

@@ -85,20 +85,6 @@ exports[`should render correctly: logged in users 1`] = `
}
}
/>
<GlobalNavPlus
appState={
Object {
"canAdmin": false,
"globalPages": Array [],
"qualifiers": Array [],
}
}
currentUser={
Object {
"isLoggedIn": true,
}
}
/>
<withRouter(GlobalNavUser)
currentUser={
Object {

+ 0
- 83
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap Просмотреть файл

@@ -1,83 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly if branches not enabled 1`] = `
<Fragment>
<Dropdown
onOpen={[Function]}
overlay={
<GlobalNavPlusMenu
canCreateApplication={false}
canCreatePortfolio={false}
canCreateProject={true}
compatibleAlms={Array []}
onComponentCreationClick={[Function]}
/>
}
tagName="li"
>
<a
className="navbar-icon navbar-plus"
href="#"
title="my_account.create_new_project_portfolio_or_application"
>
<PlusIcon />
</a>
</Dropdown>
</Fragment>
`;

exports[`should render correctly: full rights and alms 1`] = `
<Fragment>
<Dropdown
onOpen={[Function]}
overlay={
<GlobalNavPlusMenu
canCreateApplication={true}
canCreatePortfolio={true}
canCreateProject={true}
compatibleAlms={
Array [
"bitbucket",
]
}
onComponentCreationClick={[Function]}
/>
}
tagName="li"
>
<a
className="navbar-icon navbar-plus"
href="#"
title="my_account.create_new_project_portfolio_or_application"
>
<PlusIcon />
</a>
</Dropdown>
</Fragment>
`;

exports[`should render correctly: no governance 1`] = `
<Fragment>
<Dropdown
onOpen={[Function]}
overlay={
<GlobalNavPlusMenu
canCreateApplication={true}
canCreatePortfolio={false}
canCreateProject={true}
compatibleAlms={Array []}
onComponentCreationClick={[Function]}
/>
}
tagName="li"
>
<a
className="navbar-icon navbar-plus"
href="#"
title="my_account.create_new_project_portfolio_or_application"
>
<PlusIcon />
</a>
</Dropdown>
</Fragment>
`;

+ 0
- 224
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap Просмотреть файл

@@ -1,224 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: all 1`] = `
<ul
className="menu"
>
<li
className="menu-header"
>
<strong>
my_account.add_project
</strong>
</li>
<li
key="bitbucket"
>
<Link
className="display-flex-center"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "bitbucket",
},
}
}
>
<img
alt="bitbucket"
className="spacer-right"
src="/images/alm/bitbucket.svg"
width={16}
/>
my_account.add_project.bitbucket
</Link>
</li>
<li
key="manual"
>
<Link
className="display-flex-center"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "manual",
},
}
}
>
<ChevronsIcon
className="spacer-right"
/>
my_account.add_project.manual
</Link>
</li>
<li
className="divider"
/>
<li>
<ButtonLink
className="display-flex-justify-start padded-left"
onClick={[Function]}
>
<QualifierIcon
className="spacer-right"
qualifier="VW"
/>
my_account.create_new.VW
</ButtonLink>
</li>
<li>
<ButtonLink
className="display-flex-justify-start padded-left"
onClick={[Function]}
>
<QualifierIcon
className="spacer-right"
qualifier="APP"
/>
my_account.create_new.APP
</ButtonLink>
</li>
</ul>
`;

exports[`should render correctly: app only 1`] = `
<ul
className="menu"
>
<li>
<ButtonLink
className="display-flex-justify-start padded-left"
onClick={[Function]}
>
<QualifierIcon
className="spacer-right"
qualifier="APP"
/>
my_account.create_new.APP
</ButtonLink>
</li>
</ul>
`;

exports[`should render correctly: imports 1`] = `
<ul
className="menu"
>
<li
className="menu-header"
>
<strong>
my_account.add_project
</strong>
</li>
<li
key="bitbucket"
>
<Link
className="display-flex-center"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "bitbucket",
},
}
}
>
<img
alt="bitbucket"
className="spacer-right"
src="/images/alm/bitbucket.svg"
width={16}
/>
my_account.add_project.bitbucket
</Link>
</li>
<li
key="manual"
>
<Link
className="display-flex-center"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "manual",
},
}
}
>
<ChevronsIcon
className="spacer-right"
/>
my_account.add_project.manual
</Link>
</li>
</ul>
`;

exports[`should render correctly: portfolio only 1`] = `
<ul
className="menu"
>
<li>
<ButtonLink
className="display-flex-justify-start padded-left"
onClick={[Function]}
>
<QualifierIcon
className="spacer-right"
qualifier="VW"
/>
my_account.create_new.VW
</ButtonLink>
</li>
</ul>
`;

exports[`should render correctly: project only 1`] = `
<ul
className="menu"
>
<li
className="menu-header"
>
<strong>
my_account.add_project
</strong>
</li>
<li
key="manual"
>
<Link
className="display-flex-center"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "manual",
},
}
}
>
<ChevronsIcon
className="spacer-right"
/>
my_account.add_project.manual
</Link>
</li>
</ul>
`;

+ 0
- 1
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx Просмотреть файл

@@ -255,7 +255,6 @@ export class AllProjects extends React.PureComponent<Props, State> {
<div className="layout-page-main-inner">
<PageHeader
currentUser={this.props.currentUser}
isFavorite={this.props.isFavorite}
loading={this.state.loading}
onPerspectiveChange={this.handlePerspectiveChange}
onQueryChange={this.updateLocationQuery}

+ 85
- 0
server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx Просмотреть файл

@@ -0,0 +1,85 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getComponentNavigation } from '../../../api/nav';
import CreateApplicationForm from '../../../app/components/extensions/CreateApplicationForm';
import { withAppState } from '../../../components/hoc/withAppState';
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { Router, withRouter } from '../../../components/hoc/withRouter';
import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls';
import { hasGlobalPermission } from '../../../helpers/users';
import { ComponentQualifier } from '../../../types/component';

export interface ApplicationCreationProps {
appState: Pick<T.AppState, 'qualifiers'>;
className?: string;
currentUser: T.LoggedInUser;
router: Router;
}

export function ApplicationCreation(props: ApplicationCreationProps) {
const { appState, className, currentUser, router } = props;

const [showForm, setShowForm] = React.useState(false);

const canCreateApplication =
appState.qualifiers.includes(ComponentQualifier.Application) &&
hasGlobalPermission(currentUser, 'applicationcreator');

if (!canCreateApplication) {
return null;
}

const handleComponentCreate = ({
key,
qualifier
}: {
key: string;
qualifier: ComponentQualifier;
}) => {
return getComponentNavigation({ component: key }).then(({ configuration }) => {
if (configuration && configuration.showSettings) {
router.push(getComponentAdminUrl(key, qualifier));
} else {
router.push(getComponentOverviewUrl(key, qualifier));
}
setShowForm(false);
});
};

return (
<div className={className}>
<Button className="button-primary" onClick={() => setShowForm(true)}>
{translate('projects.create_application')}
</Button>

{showForm && (
<CreateApplicationForm
onClose={() => setShowForm(false)}
onCreate={handleComponentCreate}
/>
)}
</div>
);
}

export default withAppState(withCurrentUser(withRouter(ApplicationCreation)));

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx Просмотреть файл

@@ -48,7 +48,7 @@ export default class FavoriteFilter extends React.PureComponent<Props> {

return (
<header className="page-header text-center">
<div className="button-group">
<div className="button-group little-spacer-top">
<Link
activeClassName="button-active"
className="button"

+ 46
- 49
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx Просмотреть файл

@@ -22,16 +22,16 @@ import * as React from 'react';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { translate } from 'sonar-ui-common/helpers/l10n';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import { isSonarCloud } from '../../../helpers/system';
import { isLoggedIn } from '../../../helpers/users';
import SearchFilterContainer from '../filters/SearchFilterContainer';
import { Project } from '../types';
import ApplicationCreation from './ApplicationCreation';
import PerspectiveSelect from './PerspectiveSelect';
import ProjectCreationMenu from './ProjectCreationMenu';
import ProjectsSortingSelect from './ProjectsSortingSelect';

interface Props {
currentUser: T.CurrentUser;
isFavorite: boolean;
loading: boolean;
onPerspectiveChange: (x: { view: string; visualization?: string }) => void;
onQueryChange: (change: T.RawQuery) => void;
@@ -48,58 +48,55 @@ export default function PageHeader(props: Props) {
const { loading, total, projects, currentUser, view } = props;
const limitReached = projects != null && total != null && projects.length < total;
const defaultOption = isLoggedIn(currentUser) ? 'name' : 'analysis_date';
const showHomePageSelect = !isSonarCloud() || props.isFavorite;

return (
<header className="page-header projects-topbar-items">
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={props.onPerspectiveChange}
view={props.view}
visualization={props.visualization}
/>
const sortingDisabled = view === 'visualizations' && !limitReached;

{view === 'visualizations' && !limitReached ? (
<Tooltip overlay={translate('projects.sort.disabled')}>
<div className="projects-topbar-item disabled">
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption={defaultOption}
onChange={props.onSortChange}
selectedSort={props.selectedSort}
view={props.view}
/>
</div>
</Tooltip>
) : (
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption={defaultOption}
onChange={props.onSortChange}
selectedSort={props.selectedSort}
view={props.view}
/>
)}
return (
<header className="page-header">
<div className="display-flex-space-between spacer-top">
<SearchFilterContainer onQueryChange={props.onQueryChange} query={props.query} />
<div className="display-flex-center">
<ProjectCreationMenu className="little-spacer-right" />
<ApplicationCreation className="little-spacer-right" />
<HomePageSelect
className="spacer-left little-spacer-right"
currentPage={{ type: 'PROJECTS' }}
/>
</div>
</div>
<div className="big-spacer-top display-flex-space-between">
<div
className={classNames('display-flex-center', {
'is-loading': loading
})}>
{total != null && (
<span className="projects-total-label">
<strong id="projects-total">{total}</strong> {translate('projects._projects')}
</span>
)}
</div>

<SearchFilterContainer onQueryChange={props.onQueryChange} query={props.query} />
<div className="display-flex-center">
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={props.onPerspectiveChange}
view={props.view}
visualization={props.visualization}
/>

<div
className={classNames('projects-topbar-item', 'is-last', {
'is-loading': loading
})}>
{total != null && (
<span>
<strong id="projects-total">{total}</strong> {translate('projects._projects')}
</span>
)}
<Tooltip overlay={sortingDisabled ? translate('projects.sort.disabled') : undefined}>
<div className={classNames('projects-topbar-item', { disabled: sortingDisabled })}>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption={defaultOption}
onChange={props.onSortChange}
selectedSort={props.selectedSort}
view={props.view}
/>
</div>
</Tooltip>
</div>
</div>

{showHomePageSelect && (
<HomePageSelect
className="huge-spacer-left"
currentPage={isSonarCloud() ? { type: 'MY_PROJECTS' } : { type: 'PROJECTS' }}
/>
)}
</header>
);
}

+ 132
- 0
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx Просмотреть файл

@@ -0,0 +1,132 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getAlmSettings } from '../../../api/alm-settings';
import { withAppState } from '../../../components/hoc/withAppState';
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { hasGlobalPermission } from '../../../helpers/users';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import ProjectCreationMenuItem from './ProjectCreationMenuItem';

interface Props {
appState: Pick<T.AppState, 'branchesEnabled'>;
className?: string;
currentUser: T.LoggedInUser;
}

interface State {
boundAlms: Array<string>;
}

const PROJECT_CREATION_PERMISSION = 'provisioning';
/*
* ALMs for which the import feature has been implemented
*/
const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Azure, AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];

const almSettingsValidators = {
[AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url,
[AlmKeys.Bitbucket]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitHub]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitLab]: (settings: AlmSettingsInstance) => !!settings.url
};

export class ProjectCreationMenu extends React.PureComponent<Props, State> {
mounted = false;
state: State = { boundAlms: [] };

componentDidMount() {
this.mounted = true;

this.fetchAlmBindings();
}

componentWillUnmount() {
this.mounted = false;
}

almSettingIsValid = (settings: AlmSettingsInstance) => {
return almSettingsValidators[settings.alm](settings);
};

fetchAlmBindings = async () => {
const {
appState: { branchesEnabled },
currentUser
} = this.props;
const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION);

// getAlmSettings requires branchesEnabled
if (!canCreateProject || !branchesEnabled) {
return;
}

const almSettings = await getAlmSettings();

// Import is only available if exactly one binding is configured
const boundAlms = IMPORT_COMPATIBLE_ALMS.filter(key => {
const currentAlmSettings = almSettings.filter(s => s.alm === key);
return currentAlmSettings.length === 1 && this.almSettingIsValid(currentAlmSettings[0]);
});

if (this.mounted) {
this.setState({
boundAlms
});
}
};

render() {
const { className, currentUser } = this.props;
const { boundAlms } = this.state;

const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION);

if (!canCreateProject) {
return null;
}

return (
<Dropdown
className={className}
onOpen={this.fetchAlmBindings}
overlay={
<ul className="menu">
{[...boundAlms, 'manual'].map(alm => (
<li key={alm}>
<ProjectCreationMenuItem alm={alm} />
</li>
))}
</ul>
}>
<Button className="button-primary">
{translate('projects.add')}
<DropdownIcon className="spacer-left " />
</Button>
</Dropdown>
);
}
}

export default withAppState(withCurrentUser(ProjectCreationMenu));

server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx → server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx Просмотреть файл

@@ -18,24 +18,32 @@
* 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';
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';

interface Props {
defaultQualifier?: string;
onClose: () => void;
onCreate: (portfolio: { key: string; qualifier: ComponentQualifier }) => void;
export interface ProjectCreationMenuItemProps {
alm: string;
}

export default class CreateFormShim extends React.Component<Props> {
render() {
const { createFormBuilder } = (window as any).SonarGovernance;
return createFormBuilder(this.props, {
theme,
baseUrl: getBaseUrl(),
l10nBundle: getCurrentL10nBundle()
});
}
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>
);
}

+ 99
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/ApplicationCreation-test.tsx Просмотреть файл

@@ -0,0 +1,99 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow, ShallowWrapper } from 'enzyme';
import * as React from 'react';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import { getComponentNavigation } from '../../../../api/nav';
import CreateApplicationForm from '../../../../app/components/extensions/CreateApplicationForm';
import { mockAppState, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
import { ComponentQualifier } from '../../../../types/component';
import { ApplicationCreation, ApplicationCreationProps } from '../ApplicationCreation';

jest.mock('../../../../api/nav', () => ({
getComponentNavigation: jest.fn().mockResolvedValue({})
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('available');
expect(
shallowRender({ appState: mockAppState({ qualifiers: [ComponentQualifier.Portfolio] }) })
).toMatchSnapshot('unavailable');
expect(
shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: ['otherrights'] } }) })
).toMatchSnapshot('not allowed');
});

it('should show form and callback when submitted - admin', async () => {
(getComponentNavigation as jest.Mock).mockResolvedValueOnce({
configuration: { showSettings: true }
});
const routerPush = jest.fn();
const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });

await openAndSubmitForm(wrapper);

expect(routerPush).toBeCalledWith({
pathname: '/application/console',
query: {
id: 'new app'
}
});
});

it('should show form and callback when submitted - user', async () => {
(getComponentNavigation as jest.Mock).mockResolvedValueOnce({
configuration: { showSettings: false }
});
const routerPush = jest.fn();
const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });

await openAndSubmitForm(wrapper);

expect(routerPush).toBeCalledWith({
pathname: '/dashboard',
query: {
id: 'new app'
}
});
});

async function openAndSubmitForm(wrapper: ShallowWrapper) {
wrapper.find(Button).simulate('click');

const creationForm = wrapper.find(CreateApplicationForm);
expect(creationForm.exists()).toBe(true);

await creationForm
.props()
.onCreate({ key: 'new app', qualifier: ComponentQualifier.Application });
expect(getComponentNavigation).toBeCalled();
expect(wrapper.find(CreateApplicationForm).exists()).toBe(false);
}

function shallowRender(overrides: Partial<ApplicationCreationProps> = {}) {
return shallow(
<ApplicationCreation
appState={mockAppState({ qualifiers: [ComponentQualifier.Application] })}
currentUser={mockLoggedInUser({ permissions: { global: ['applicationcreator'] } })}
router={mockRouter()}
{...overrides}
/>
);
}

+ 0
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx Просмотреть файл

@@ -71,7 +71,6 @@ function shallowRender(props?: {}) {
return shallow(
<PageHeader
currentUser={{ isLoggedIn: false }}
isFavorite={false}
loading={false}
onPerspectiveChange={jest.fn()}
onQueryChange={jest.fn()}

+ 85
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx Просмотреть файл

@@ -0,0 +1,85 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getAlmSettings } from '../../../../api/alm-settings';
import { mockAppState, mockLoggedInUser } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings';
import { ProjectCreationMenu } from '../ProjectCreationMenu';

jest.mock('../../../../api/alm-settings', () => ({
getAlmSettings: jest.fn().mockResolvedValue([])
}));

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(
shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) })
).toMatchSnapshot('not allowed');
});

it('should fetch alm bindings on mount', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(getAlmSettings).toBeCalled();
});

it('should not fetch alm bindings if user cannot create projects', async () => {
const wrapper = shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) });
await waitAndUpdate(wrapper);
expect(getAlmSettings).not.toBeCalled();
});

it('should not fetch alm bindings if branches are not enabled', async () => {
const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: false }) });
await waitAndUpdate(wrapper);
expect(getAlmSettings).not.toBeCalled();
});

it('should filter alm bindings appropriately', async () => {
(getAlmSettings as jest.Mock).mockResolvedValueOnce([
{ alm: AlmKeys.Azure },
{ alm: AlmKeys.Bitbucket, url: 'b1' },
{ alm: AlmKeys.Bitbucket, url: 'b2' },
{ alm: AlmKeys.GitHub },
{ alm: AlmKeys.GitLab, url: 'gitlab.com' }
]);

const wrapper = shallowRender();

await waitAndUpdate(wrapper);

expect(wrapper.state().boundAlms).toEqual([AlmKeys.GitHub, AlmKeys.GitLab]);
});

function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) {
return shallow<ProjectCreationMenu>(
<ProjectCreationMenu
appState={mockAppState({ branchesEnabled: true })}
currentUser={mockLoggedInUser({ permissions: { global: ['provisioning'] } })}
{...overrides}
/>
);
}

server/sonar-web/src/main/js/apps/portfolio/components/__tests__/CreateFormShim-test.tsx → server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx Просмотреть файл

@@ -19,17 +19,14 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import CreateFormShim from '../CreateFormShim';
import { AlmKeys } from '../../../../types/alm-settings';
import ProjectCreationMenuItem, { ProjectCreationMenuItemProps } from '../ProjectCreationMenuItem';

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();
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('bitbucket');
expect(shallowRender({ alm: 'manual' })).toMatchSnapshot('manual');
});

function shallowRender() {
return shallow(<CreateFormShim onClose={jest.fn()} onCreate={jest.fn()} />);
function shallowRender(overrides: Partial<ProjectCreationMenuItemProps> = {}) {
return shallow(<ProjectCreationMenuItem alm={AlmKeys.Bitbucket} {...overrides} />);
}

+ 0
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap Просмотреть файл

@@ -66,7 +66,6 @@ exports[`renders 1`] = `
"isLoggedIn": true,
}
}
isFavorite={false}
loading={false}
onPerspectiveChange={[Function]}
onQueryChange={[Function]}
@@ -220,7 +219,6 @@ exports[`renders 2`] = `
"isLoggedIn": true,
}
}
isFavorite={false}
loading={false}
onPerspectiveChange={[Function]}
onQueryChange={[Function]}

+ 16
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ApplicationCreation-test.tsx.snap Просмотреть файл

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: available 1`] = `
<div>
<Button
className="button-primary"
onClick={[Function]}
>
projects.create_application
</Button>
</div>
`;

exports[`should render correctly: not allowed 1`] = `""`;

exports[`should render correctly: unavailable 1`] = `""`;

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/FavoriteFilter-test.tsx.snap Просмотреть файл

@@ -5,7 +5,7 @@ exports[`renders for logged in user 1`] = `
className="page-header text-center"
>
<div
className="button-group"
className="button-group little-spacer-top"
>
<Link
activeClassName="button-active"

+ 192
- 110
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap Просмотреть файл

@@ -2,150 +2,232 @@

exports[`should render correctly 1`] = `
<header
className="page-header projects-topbar-items"
className="page-header"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={[MockFunction]}
view="overall"
/>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
<SearchFilterContainer
onQueryChange={[MockFunction]}
query={
Object {
"search": "test",
<div
className="display-flex-space-between spacer-top"
>
<SearchFilterContainer
onQueryChange={[MockFunction]}
query={
Object {
"search": "test",
}
}
}
/>
/>
<div
className="display-flex-center"
>
<Connect(withAppState(Connect(withCurrentUser(ProjectCreationMenu))))
className="little-spacer-right"
/>
<Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation)))))
className="little-spacer-right"
/>
<Connect(HomePageSelect)
className="spacer-left little-spacer-right"
currentPage={
Object {
"type": "PROJECTS",
}
}
/>
</div>
</div>
<div
className="projects-topbar-item is-last"
className="big-spacer-top display-flex-space-between"
>
<span>
<strong
id="projects-total"
<div
className="display-flex-center"
>
<span
className="projects-total-label"
>
12
</strong>
projects._projects
</span>
<strong
id="projects-total"
>
12
</strong>
projects._projects
</span>
</div>
<div
className="display-flex-center"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={[MockFunction]}
view="overall"
/>
<Tooltip>
<div
className="projects-topbar-item"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
</div>
</Tooltip>
</div>
</div>
<Connect(HomePageSelect)
className="huge-spacer-left"
currentPage={
Object {
"type": "PROJECTS",
}
}
/>
</header>
`;

exports[`should render correctly while loading 1`] = `
<header
className="page-header projects-topbar-items"
className="page-header"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={[MockFunction]}
view="overall"
/>
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
<SearchFilterContainer
onQueryChange={[MockFunction]}
query={
Object {
"search": "test",
<div
className="display-flex-space-between spacer-top"
>
<SearchFilterContainer
onQueryChange={[MockFunction]}
query={
Object {
"search": "test",
}
}
}
/>
/>
<div
className="display-flex-center"
>
<Connect(withAppState(Connect(withCurrentUser(ProjectCreationMenu))))
className="little-spacer-right"
/>
<Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation)))))
className="little-spacer-right"
/>
<Connect(HomePageSelect)
className="spacer-left little-spacer-right"
currentPage={
Object {
"type": "PROJECTS",
}
}
/>
</div>
</div>
<div
className="projects-topbar-item is-last is-loading"
className="big-spacer-top display-flex-space-between"
>
<span>
<strong
id="projects-total"
<div
className="display-flex-center is-loading"
>
<span
className="projects-total-label"
>
2
</strong>
projects._projects
</span>
<strong
id="projects-total"
>
2
</strong>
projects._projects
</span>
</div>
<div
className="display-flex-center"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={[MockFunction]}
view="overall"
/>
<Tooltip>
<div
className="projects-topbar-item"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
</div>
</Tooltip>
</div>
</div>
<Connect(HomePageSelect)
className="huge-spacer-left"
currentPage={
Object {
"type": "PROJECTS",
}
}
/>
</header>
`;

exports[`should render disabled sorting options for visualizations 1`] = `
<header
className="page-header projects-topbar-items"
className="page-header"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={[MockFunction]}
view="visualizations"
visualization="coverage"
/>
<Tooltip
overlay="projects.sort.disabled"
<div
className="display-flex-space-between spacer-top"
>
<SearchFilterContainer
onQueryChange={[MockFunction]}
query={
Object {
"search": "test",
}
}
/>
<div
className="display-flex-center"
>
<Connect(withAppState(Connect(withCurrentUser(ProjectCreationMenu))))
className="little-spacer-right"
/>
<Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation)))))
className="little-spacer-right"
/>
<Connect(HomePageSelect)
className="spacer-left little-spacer-right"
currentPage={
Object {
"type": "PROJECTS",
}
}
/>
</div>
</div>
<div
className="big-spacer-top display-flex-space-between"
>
<div
className="projects-topbar-item disabled"
className="display-flex-center"
/>
<div
className="display-flex-center"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={[MockFunction]}
selectedSort="size"
view="visualizations"
visualization="coverage"
/>
<Tooltip
overlay="projects.sort.disabled"
>
<div
className="projects-topbar-item disabled"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="visualizations"
/>
</div>
</Tooltip>
</div>
</Tooltip>
<SearchFilterContainer
onQueryChange={[MockFunction]}
query={
Object {
"search": "test",
}
}
/>
<div
className="projects-topbar-item is-last"
/>
<Connect(HomePageSelect)
className="huge-spacer-left"
currentPage={
Object {
"type": "PROJECTS",
}
}
/>
</div>
</header>
`;

exports[`should render switch the default sorting option for anonymous users 1`] = `
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
className="js-projects-sorting-select"
defaultOption="name"
onChange={[MockFunction]}
selectedSort="size"
@@ -155,7 +237,7 @@ exports[`should render switch the default sorting option for anonymous users 1`]

exports[`should render switch the default sorting option for anonymous users 2`] = `
<ProjectsSortingSelect
className="projects-topbar-item js-projects-sorting-select"
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"

+ 29
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenu-test.tsx.snap Просмотреть файл

@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<Dropdown
onOpen={[Function]}
overlay={
<ul
className="menu"
>
<li>
<ProjectCreationMenuItem
alm="manual"
/>
</li>
</ul>
}
>
<Button
className="button-primary"
>
projects.add
<DropdownIcon
className="spacer-left "
/>
</Button>
</Dropdown>
`;

exports[`should render correctly: not allowed 1`] = `""`;

+ 46
- 0
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCreationMenuItem-test.tsx.snap Просмотреть файл

@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: bitbucket 1`] = `
<Link
className="display-flex-center"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "bitbucket",
},
}
}
>
<img
alt="bitbucket"
className="spacer-right"
src="/images/alm/bitbucket.svg"
width={16}
/>
my_account.add_project.bitbucket
</Link>
`;

exports[`should render correctly: manual 1`] = `
<Link
className="display-flex-center"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "manual",
},
}
}
>
<ChevronsIcon
className="spacer-right"
/>
my_account.add_project.manual
</Link>
`;

+ 4
- 4
server/sonar-web/src/main/js/apps/projects/styles.css Просмотреть файл

@@ -17,10 +17,10 @@
* 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 {

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Просмотреть файл

@@ -938,6 +938,8 @@ issue_bulk_change.no_match=There is no issue matching your filter selection

projects.page=Projects
projects._projects=projects
projects.add=Add project
projects.create_application=Create Application
projects.no_projects.empty_instance=There are no visible projects yet.
projects.no_projects.empty_instance.new_project=Once you analyze some projects, they will show up here.
projects.no_projects.empty_instance.how_to_add_projects=Here is how you can analyze new projects

Загрузка…
Отмена
Сохранить