@@ -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); |
@@ -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> | |||
); | |||
} |
@@ -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); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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); |
@@ -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; |
@@ -68,7 +68,7 @@ exports[`should render correctly: no projects 1`] = ` | |||
Object { | |||
"pathname": "/projects/create", | |||
"query": Object { | |||
"mode": "bbs", | |||
"mode": "bitbucket", | |||
"resetPat": 1, | |||
}, | |||
} |
@@ -263,7 +263,7 @@ exports[`should render correctly: no repos 1`] = ` | |||
Object { | |||
"pathname": "/projects/create", | |||
"query": Object { | |||
"mode": "bbs", | |||
"mode": "bitbucket", | |||
"resetPat": 1, | |||
}, | |||
} |
@@ -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 {}, |
@@ -19,5 +19,5 @@ | |||
*/ | |||
export enum CreateProjectModes { | |||
Manual = 'manual', | |||
BitbucketServer = 'bbs' | |||
BitbucketServer = 'bitbucket' | |||
} |
@@ -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> { |
@@ -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 |