@@ -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> |
@@ -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); |
@@ -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> | |||
); | |||
} |
@@ -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()} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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 { |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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} |
@@ -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))); |
@@ -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" |
@@ -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> | |||
); | |||
} |
@@ -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)); |
@@ -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> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -71,7 +71,6 @@ function shallowRender(props?: {}) { | |||
return shallow( | |||
<PageHeader | |||
currentUser={{ isLoggedIn: false }} | |||
isFavorite={false} | |||
loading={false} | |||
onPerspectiveChange={jest.fn()} | |||
onQueryChange={jest.fn()} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} />); | |||
} |
@@ -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]} |
@@ -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`] = `""`; |
@@ -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" |
@@ -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" |
@@ -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`] = `""`; |
@@ -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> | |||
`; |
@@ -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 { |
@@ -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 |