Browse Source

SONAR-13479 new creation menu

tags/8.4.0.35506
Jeremy Davis 4 years ago
parent
commit
0f5ec7677c

+ 61
- 97
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx View File

@@ -18,33 +18,46 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Link, withRouter, WithRouterProps } from 'react-router';
import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
import PlusIcon from 'sonar-ui-common/components/icons/PlusIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getAlmSettings } from '../../../../api/alm-settings';
import { getComponentNavigation } from '../../../../api/nav';
import CreateFormShim from '../../../../apps/portfolio/components/CreateFormShim';
import { Router, withRouter } from '../../../../components/hoc/withRouter';
import { getExtensionStart } from '../../../../helpers/extensions';
import { isSonarCloud } from '../../../../helpers/system';
import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../helpers/urls';
import { hasGlobalPermission } from '../../../../helpers/users';
import { AlmKeys } from '../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../types/component';
import GlobalNavPlusMenu from './GlobalNavPlusMenu';

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

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

export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps, State> {
/*
* ALMs for which the import feature has been implemented
*/
const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Bitbucket, AlmKeys.GitHub];

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

componentDidMount() {
this.mounted = true;

this.fetchAlmBindings();

if (this.props.appState.qualifiers.includes('VW')) {
getExtensionStart('governance/console').then(
() => {
@@ -61,26 +74,31 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
this.mounted = false;
}

handleNewProjectClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.router.push('/projects/create');
closeComponentCreationForm = () => {
this.setState({ creatingComponent: undefined });
};

openCreatePortfolioForm = () => {
this.setState({ createPortfolio: true });
};
fetchAlmBindings = async () => {
const almSettings = await getAlmSettings();

// Import is only available if exactly one binding is configured
const boundAlms = IMPORT_COMPATIBLE_ALMS.filter(key => {
const count = almSettings.filter(s => s.alm === key).length;
return count === 1;
});

closeCreatePortfolioForm = () => {
this.setState({ createPortfolio: false });
if (this.mounted) {
this.setState({
boundAlms
});
}
};

handleNewPortfolioClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.openCreatePortfolioForm();
handleComponentCreationClick = (qualifier: ComponentQualifier) => {
this.setState({ creatingComponent: qualifier });
};

handleCreatePortfolio = ({ key, qualifier }: { key: string; qualifier: string }) => {
handleComponentCreate = ({ key, qualifier }: { key: string; qualifier: ComponentQualifier }) => {
return getComponentNavigation({ component: key }).then(data => {
if (
data.configuration &&
@@ -93,104 +111,50 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
} else {
this.props.router.push(getPortfolioUrl(key));
}
this.closeCreatePortfolioForm();
this.closeComponentCreationForm();
});
};

renderCreateProject(canCreateProject: boolean) {
if (!canCreateProject) {
return null;
}
return (
<li>
<a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
{isSonarCloud()
? translate('provisioning.analyze_new_project')
: translate('my_account.create_new.TRK')}
</a>
</li>
);
}

renderCreateOrganization(canCreateOrg: boolean) {
if (!canCreateOrg) {
return null;
}

return (
<li>
<Link className="js-new-organization" to="/create-organization">
{translate('my_account.create_new_organization')}
</Link>
</li>
);
}

renderCreatePortfolio(showGovernanceEntry: boolean, defaultQualifier?: string) {
const governanceInstalled = this.props.appState.qualifiers.includes('VW');
if (!governanceInstalled || !showGovernanceEntry) {
return null;
}

return (
<li>
<a className="js-new-portfolio" href="#" onClick={this.handleNewPortfolioClick}>
{defaultQualifier
? translate('my_account.create_new', defaultQualifier)
: translate('my_account.create_new_portfolio_application')}
</a>
</li>
);
}

render() {
const { currentUser } = this.props;
const canCreateApplication = hasGlobalPermission(currentUser, 'applicationcreator');
const canCreateOrg = isSonarCloud();
const canCreatePortfolio = hasGlobalPermission(currentUser, 'portfoliocreator');
const canCreateProject = isSonarCloud() || hasGlobalPermission(currentUser, 'provisioning');

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

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

let defaultQualifier: string | undefined;
if (!canCreateApplication) {
defaultQualifier = 'VW';
} else if (!canCreatePortfolio) {
defaultQualifier = 'APP';
}

return (
<>
<Dropdown
onOpen={canCreateProject ? this.fetchAlmBindings : undefined}
overlay={
<ul className="menu">
{this.renderCreateProject(canCreateProject)}
{this.renderCreateOrganization(canCreateOrg)}
{this.renderCreatePortfolio(
canCreateApplication || canCreatePortfolio,
defaultQualifier
)}
</ul>
<GlobalNavPlusMenu
canCreateApplication={canCreateApplication}
canCreatePortfolio={canCreatePortfolio}
canCreateProject={canCreateProject}
compatibleAlms={boundAlms}
onComponentCreationClick={this.handleComponentCreationClick}
/>
}
tagName="li">
<a
className="navbar-icon navbar-plus"
href="#"
title={
isSonarCloud()
? translate('my_account.create_new_project_or_organization')
: translate('my_account.create_new_project_portfolio_or_application')
}>
title={translate('my_account.create_new_project_portfolio_or_application')}>
<PlusIcon />
</a>
</Dropdown>
{this.state.governanceReady && this.state.createPortfolio && (
{governanceReady && creatingComponent && (
<CreateFormShim
defaultQualifier={defaultQualifier}
onClose={this.closeCreatePortfolioForm}
onCreate={this.handleCreatePortfolio}
defaultQualifier={creatingComponent}
onClose={this.closeComponentCreationForm}
onCreate={this.handleComponentCreate}
/>
)}
</>
@@ -198,4 +162,4 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
}
}

export default withRouter<Props>(GlobalNavPlus);
export default withRouter(GlobalNavPlus);

+ 99
- 0
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlusMenu.tsx View File

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

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

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

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

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

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

+ 79
- 62
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx View File

@@ -17,88 +17,105 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow, ShallowWrapper } from 'enzyme';
import { shallow } from 'enzyme';
import * as React from 'react';
import { click } from 'sonar-ui-common/helpers/testUtils';
import { isSonarCloud } from '../../../../../helpers/system';
import { mockRouter } from '../../../../../helpers/testMocks';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getAlmSettings } from '../../../../../api/alm-settings';
import { getComponentNavigation } from '../../../../../api/nav';
import CreateFormShim from '../../../../../apps/portfolio/components/CreateFormShim';
import { mockLoggedInUser, mockRouter } from '../../../../../helpers/testMocks';
import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../../helpers/urls';
import { AlmKeys } from '../../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../../types/component';
import { GlobalNavPlus } from '../GlobalNavPlus';

jest.mock('../../../../../helpers/system', () => ({
isSonarCloud: jest.fn()
const PROJECT_CREATION_RIGHT = 'provisioning';
const APP_CREATION_RIGHT = 'applicationcreator';
const PORTFOLIO_CREATION_RIGHT = 'portfoliocreator';

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

beforeEach(() => {
(isSonarCloud as jest.Mock).mockReturnValue(false);
});
jest.mock('../../../../../api/nav', () => ({
getComponentNavigation: jest.fn().mockResolvedValue({})
}));

it('render', () => {
const wrapper = getWrapper();
expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
jest.mock('../../../../../helpers/urls', () => ({
getPortfolioUrl: jest.fn(),
getPortfolioAdminUrl: jest.fn()
}));

it('opens onboarding', () => {
const push = jest.fn();
const wrapper = getOverlayWrapper(getWrapper({ router: mockRouter({ push }) }));
click(wrapper.find('.js-new-project'));
expect(push).toBeCalled();
});
it('should render correctly', () => {
expect(shallowRender().type()).toBeNull();
expect(
shallowRender([APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT])
).toMatchSnapshot('no governance');

it('should display create new project link when user has permission only', () => {
expect(getWrapper({}, []).find('Dropdown').length).toEqual(0);
const wrapper = shallowRender(
[APP_CREATION_RIGHT, PORTFOLIO_CREATION_RIGHT, PROJECT_CREATION_RIGHT],
true
);
wrapper.setState({ boundAlms: ['bitbucket'] });
expect(wrapper).toMatchSnapshot('full rights and alms');
});

it('should display create new organization on SonarCloud only', () => {
(isSonarCloud as jest.Mock).mockReturnValue(true);
expect(getOverlayWrapper(getWrapper())).toMatchSnapshot();
});
it('should load correctly', async () => {
(getAlmSettings as jest.Mock).mockResolvedValueOnce([
{ alm: AlmKeys.Azure, key: 'A1' },
{ alm: AlmKeys.Bitbucket, key: 'B1' },
{ alm: AlmKeys.GitHub, key: 'GH1' }
]);

it('should display new organization and new project on SonarCloud', () => {
(isSonarCloud as jest.Mock).mockReturnValue(true);
expect(getOverlayWrapper(getWrapper({}, []))).toMatchSnapshot();
});
const wrapper = shallowRender();

it('should display create portfolio and application', () => {
checkOpenCreatePortfolio(['applicationcreator', 'portfoliocreator'], undefined);
await waitAndUpdate(wrapper);

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

it('should display create portfolio', () => {
checkOpenCreatePortfolio(['portfoliocreator'], 'VW');
it('should display component creation form', () => {
const wrapper = shallowRender([PORTFOLIO_CREATION_RIGHT], true);

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

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

it('should display create application', () => {
checkOpenCreatePortfolio(['applicationcreator'], 'APP');
describe('handleComponentCreate', () => {
(getComponentNavigation as jest.Mock)
.mockResolvedValueOnce({
configuration: { extensions: [{ key: 'governance/console', name: 'governance' }] }
})
.mockResolvedValueOnce({});

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

const wrapper = shallowRender([], true);

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

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

expect(getPortfolioUrl).toBeCalledWith(portfolio.key);
});
});

function getWrapper(props = {}, globalPermissions?: string[]) {
return shallow(
// @ts-ignore avoid passing everything from WithRouterProps
function shallowRender(permissions: string[] = [], enableGovernance = false) {
return shallow<GlobalNavPlus>(
<GlobalNavPlus
appState={{ qualifiers: [] }}
currentUser={
{
isLoggedIn: true,
permissions: { global: globalPermissions || ['provisioning'] }
} as T.LoggedInUser
}
appState={{ qualifiers: enableGovernance ? [ComponentQualifier.Portfolio] : [] }}
currentUser={mockLoggedInUser({ permissions: { global: permissions } })}
router={mockRouter()}
{...props}
/>
);
}

function getOverlayWrapper(wrapper: ShallowWrapper) {
return shallow(wrapper.find('Dropdown').prop('overlay'));
}

function checkOpenCreatePortfolio(permissions: string[], defaultQualifier?: string) {
const wrapper = getWrapper({ appState: { qualifiers: ['VW'] } }, permissions);
wrapper.setState({ governanceReady: true });
const overlayWrapper = getOverlayWrapper(wrapper);
expect(overlayWrapper.find('.js-new-portfolio')).toMatchSnapshot();

click(overlayWrapper.find('.js-new-portfolio'));
wrapper.update();
expect(wrapper.find('CreateFormShim').exists()).toBe(true);
expect(wrapper.find('CreateFormShim').prop('defaultQualifier')).toBe(defaultQualifier);
}

+ 76
- 0
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlusMenu-test.tsx View File

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

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

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

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

onComponentCreationClick.mockClear();

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

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

+ 43
- 98
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap View File

@@ -1,112 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

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

exports[`should display create application 1`] = `
<a
className="js-new-portfolio"
href="#"
onClick={[Function]}
>
my_account.create_new.APP
</a>
`;

exports[`should display create new organization on SonarCloud only 1`] = `
<ul
className="menu"
>
<li>
<a
className="js-new-project"
className="navbar-icon navbar-plus"
href="#"
onClick={[Function]}
title="my_account.create_new_project_portfolio_or_application"
>
provisioning.analyze_new_project
<PlusIcon />
</a>
</li>
<li>
<Link
className="js-new-organization"
onlyActiveOnIndex={false}
style={Object {}}
to="/create-organization"
>
my_account.create_new_organization
</Link>
</li>
</ul>
</Dropdown>
</Fragment>
`;

exports[`should display create portfolio 1`] = `
<a
className="js-new-portfolio"
href="#"
onClick={[Function]}
>
my_account.create_new.VW
</a>
`;

exports[`should display create portfolio and application 1`] = `
<a
className="js-new-portfolio"
href="#"
onClick={[Function]}
>
my_account.create_new_portfolio_application
</a>
`;

exports[`should display new organization and new project on SonarCloud 1`] = `
<ul
className="menu"
>
<li>
exports[`should render correctly: no governance 1`] = `
<Fragment>
<Dropdown
onOpen={[Function]}
overlay={
<GlobalNavPlusMenu
canCreateApplication={false}
canCreatePortfolio={false}
canCreateProject={true}
compatibleAlms={Array []}
onComponentCreationClick={[Function]}
/>
}
tagName="li"
>
<a
className="js-new-project"
className="navbar-icon navbar-plus"
href="#"
onClick={[Function]}
title="my_account.create_new_project_portfolio_or_application"
>
provisioning.analyze_new_project
<PlusIcon />
</a>
</li>
<li>
<Link
className="js-new-organization"
onlyActiveOnIndex={false}
style={Object {}}
to="/create-organization"
>
my_account.create_new_organization
</Link>
</li>
</ul>
</Dropdown>
</Fragment>
`;

+ 224
- 0
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlusMenu-test.tsx.snap View File

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

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

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

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

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

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

+ 10
- 1
server/sonar-web/src/main/js/app/styles/components/menu.css View File

@@ -37,6 +37,7 @@

.menu-item,
.menu > li > a,
.menu > li > button,
.menu > li > span {
display: block;
padding: 4px 16px;
@@ -54,6 +55,12 @@
transition: none;
}

.menu > li > button {
color: var(--baseFontColor);
text-align: left;
width: 100%;
}

.menu > li > a.rich-item {
display: flex;
align-items: center;
@@ -82,7 +89,9 @@
}

.menu > li > a:hover,
.menu > li > a:focus {
.menu > li > a:focus,
.menu > li > button:hover,
.menu > li > button:focus {
text-decoration: none;
color: var(--baseFontColor);
background-color: var(--barBackgroundColor);

+ 5
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css View File

@@ -395,6 +395,11 @@ th.huge-spacer-right {
align-items: center;
}

.display-flex-justify-start {
display: flex !important;
justify-content: flex-start !important;
}

.display-flex-justify-center {
display: flex !important;
justify-content: center;

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketImportRepositoryForm-test.tsx.snap View File

@@ -68,7 +68,7 @@ exports[`should render correctly: no projects 1`] = `
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "bbs",
"mode": "bitbucket",
"resetPat": 1,
},
}

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectAccordion-test.tsx.snap View File

@@ -263,7 +263,7 @@ exports[`should render correctly: no repos 1`] = `
Object {
"pathname": "/projects/create",
"query": Object {
"mode": "bbs",
"mode": "bitbucket",
"resetPat": 1,
},
}

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap View File

@@ -81,7 +81,7 @@ exports[`should render correctly if the BBS method is selected 1`] = `
"key": "key",
"pathname": "/path",
"query": Object {
"mode": "bbs",
"mode": "bitbucket",
},
"search": "",
"state": Object {},

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/types.ts View File

@@ -19,5 +19,5 @@
*/
export enum CreateProjectModes {
Manual = 'manual',
BitbucketServer = 'bbs'
BitbucketServer = 'bitbucket'
}

+ 2
- 1
server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx View File

@@ -21,11 +21,12 @@ import * as React from 'react';
import * as theme from '../../../app/theme';
import { getCurrentL10nBundle } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { ComponentQualifier } from '../../../types/component';

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

export default class CreateFormShim extends React.Component<Props> {

+ 7
- 6
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1787,12 +1787,14 @@ my_account.create_organization=Create Organization
my_account.search_project=Search Project
my_account.set_notifications_for=Search a project by name
my_account.set_notifications_for.title=Add a project
my_account.create_new_portfolio_application=Create new portfolio / application
my_account.create_new.TRK=Create new project
my_account.create_new.VW=Create new portfolio
my_account.create_new.APP=Create new application
my_account.create_new_organization=Create new organization
my_account.create_new_project_or_organization=Analyze new project or create new organization
my_account.create_new.VW=Create portfolio
my_account.create_new.APP=Create application
my_account.add_project=Add project
my_account.add_project.manual=Manually
my_account.add_project.github=GitHub
my_account.add_project.bitbucket=Bitbucket

my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application


@@ -1801,7 +1803,6 @@ my_account.create_new_project_portfolio_or_application=Analyze new project / Cre
# PROJECT PROVISIONING
#
#------------------------------------------------------------------------------
provisioning.analyze_new_project=Analyze new project
provisioning.no_analysis=No analysis has been performed since creation. The only available section is the configuration.
provisioning.no_analysis.delete=Either you should retry analysis or simply {link}.
provisioning.no_analysis.delete_project=delete the project

Loading…
Cancel
Save