aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx22
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx9
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx9
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx17
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx175
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx81
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap71
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx33
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts4
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties4
12 files changed, 335 insertions, 150 deletions
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
index c62d51dfd5e..5cc79d2d179 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
@@ -18,33 +18,20 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import * as PropTypes from 'prop-types';
import { Link } from 'react-router';
import ProductNewsMenuItem from './ProductNewsMenuItem';
import { SuggestionLink } from './SuggestionsProvider';
-import { CurrentUser, isLoggedIn, hasGlobalPermission } from '../../types';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
import { isSonarCloud } from '../../../helpers/system';
import { DropdownOverlay } from '../../../components/controls/Dropdown';
interface Props {
- currentUser: CurrentUser;
onClose: () => void;
suggestions: Array<SuggestionLink>;
}
export default class EmbedDocsPopup extends React.PureComponent<Props> {
- static contextTypes = {
- openProjectOnboarding: PropTypes.func
- };
-
- onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
- event.preventDefault();
- event.currentTarget.blur();
- this.context.openProjectOnboarding();
- };
-
renderTitle(text: string) {
return <li className="menu-header">{text}</li>;
}
@@ -119,17 +106,8 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
}
renderSonarQubeLinks() {
- const { currentUser } = this.props;
return (
<React.Fragment>
- {isLoggedIn(currentUser) &&
- hasGlobalPermission(currentUser, 'provisioning') && (
- <li>
- <a data-test="analyze-new-project" href="#" onClick={this.onAnalyzeProjectClick}>
- {translate('embed_docs.analyze_new_project')}
- </a>
- </li>
- )}
<li className="divider" />
<li>
<a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank">
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
index ff39f16ad71..8e2486b5025 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import { SuggestionLink } from './SuggestionsProvider';
-import { CurrentUser } from '../../types';
import Toggler from '../../../components/controls/Toggler';
import HelpIcon from '../../../components/icons-components/HelpIcon';
import { lazyLoad } from '../../../components/lazyLoad';
@@ -28,9 +27,7 @@ import { translate } from '../../../helpers/l10n';
const EmbedDocsPopup = lazyLoad(() => import('./EmbedDocsPopup'));
interface Props {
- currentUser: CurrentUser;
suggestions: Array<SuggestionLink>;
- tooltip: boolean;
}
interface State {
helpOpen: boolean;
@@ -85,11 +82,7 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<Props, Sta
onRequestClose={this.closeHelp}
open={this.state.helpOpen}
overlay={
- <EmbedDocsPopup
- currentUser={this.props.currentUser}
- onClose={this.closeHelp}
- suggestions={this.props.suggestions}
- />
+ <EmbedDocsPopup onClose={this.closeHelp} suggestions={this.props.suggestions} />
}>
<a className="navbar-help" href="#" onClick={this.handleClick} title={translate('help')}>
<HelpIcon />
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
index 24f5fbec4d6..e21c57fb020 100644
--- a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
@@ -28,55 +28,19 @@ const suggestions = [{ link: '#', text: 'foo' }, { link: '#', text: 'bar' }];
it('should display suggestion links', () => {
const context = {};
- const wrapper = shallow(
- <EmbedDocsPopup
- currentUser={{ isLoggedIn: true }}
- onClose={jest.fn()}
- suggestions={suggestions}
- />,
- {
- context
- }
- );
+ const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, {
+ context
+ });
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
-it('should display analyze new project link when user has permission', () => {
- const wrapper = shallow(
- <EmbedDocsPopup
- currentUser={{ isLoggedIn: true, permissions: { global: ['provisioning'] } }}
- onClose={jest.fn()}
- suggestions={suggestions}
- />
- );
- expect(wrapper.find('[data-test="analyze-new-project"]').exists()).toBe(true);
-});
-
-it('should not display analyze new project link when user does not have permission', () => {
- const wrapper = shallow(
- <EmbedDocsPopup
- currentUser={{ isLoggedIn: true }}
- onClose={jest.fn()}
- suggestions={suggestions}
- />
- );
- expect(wrapper.find('[data-test="analyze-new-project"]').exists()).toBe(false);
-});
-
it('should display correct links for SonarCloud', () => {
(isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true);
const context = {};
- const wrapper = shallow(
- <EmbedDocsPopup
- currentUser={{ isLoggedIn: true }}
- onClose={jest.fn()}
- suggestions={suggestions}
- />,
- {
- context
- }
- );
+ const wrapper = shallow(<EmbedDocsPopup onClose={jest.fn()} suggestions={suggestions} />, {
+ context
+ });
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
index 069472c96e5..fbdad095b35 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
@@ -38,6 +38,7 @@ import HelpTooltip from '../../../../components/controls/HelpTooltip';
import Toggler from '../../../../components/controls/Toggler';
import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
import { isSonarCloud } from '../../../../helpers/system';
+import { getPortfolioAdminUrl } from '../../../../helpers/urls';
interface Props {
branchLikes: BranchLike[];
@@ -128,15 +129,13 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
};
renderOverlay = () => {
- const adminLink = {
- pathname: '/project/admin/extension/governance/console',
- query: { id: this.props.component.breadcrumbs[0].key, qualifier: 'APP' }
- };
return (
<>
<p>{translate('application.branches.help')}</p>
<hr className="spacer-top spacer-bottom" />
- <Link className="spacer-left link-no-underline" to={adminLink}>
+ <Link
+ className="spacer-left link-no-underline"
+ to={getPortfolioAdminUrl(this.props.component.breadcrumbs[0].key, 'APP')}>
{translate('application.branches.link')}
</Link>
</>
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
index 396050e4731..2c1aa0f6813 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
@@ -61,16 +61,15 @@ export class GlobalNav extends React.PureComponent<Props> {
<ul className="global-navbar-menu global-navbar-menu-right">
{isSonarCloud() && <GlobalNavExplore location={this.props.location} />}
- <EmbedDocsPopupHelper
- currentUser={this.props.currentUser}
- suggestions={this.props.suggestions}
- tooltip={!isSonarCloud()}
- />
+ <EmbedDocsPopupHelper suggestions={this.props.suggestions} />
<Search appState={this.props.appState} currentUser={this.props.currentUser} />
- {isLoggedIn(this.props.currentUser) &&
- isSonarCloud() && (
- <GlobalNavPlus openProjectOnboarding={this.context.openProjectOnboarding} />
- )}
+ {isLoggedIn(this.props.currentUser) && (
+ <GlobalNavPlus
+ appState={this.props.appState}
+ currentUser={this.props.currentUser}
+ openProjectOnboarding={this.context.openProjectOnboarding}
+ />
+ )}
<GlobalNavUserContainer {...this.props} />
</ul>
</NavBar>
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
index 57f21824533..e702ca72b3d 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
@@ -18,47 +18,166 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Link } from 'react-router';
-import PlusIcon from '../../../../components/icons-components/PlusIcon';
+import { Link, withRouter, WithRouterProps } from 'react-router';
+import CreateFormShim from '../../../../apps/portfolio/components/CreateFormShim';
import Dropdown from '../../../../components/controls/Dropdown';
+import PlusIcon from '../../../../components/icons-components/PlusIcon';
+import { AppState, hasGlobalPermission, CurrentUser } from '../../../types';
+import { getPortfolioAdminUrl } from '../../../../helpers/urls';
+import { getExtensionStart } from '../../extensions/utils';
+import { isSonarCloud } from '../../../../helpers/system';
import { translate } from '../../../../helpers/l10n';
interface Props {
+ appState: Pick<AppState, 'qualifiers'>;
+ currentUser: CurrentUser;
openProjectOnboarding: () => void;
}
-export default class GlobalNavPlus extends React.PureComponent<Props> {
- handleNewProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+interface State {
+ createPortfolio: boolean;
+ governanceReady: boolean;
+}
+
+export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps, State> {
+ mounted = false;
+ state: State = { createPortfolio: false, governanceReady: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.props.appState.qualifiers.includes('VW')) {
+ getExtensionStart('governance/console').then(
+ () => {
+ if (this.mounted) {
+ this.setState({ governanceReady: true });
+ }
+ },
+ () => {}
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleNewProjectClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.openProjectOnboarding();
};
- render() {
+ openCreatePortfolioForm = () => {
+ this.setState({ createPortfolio: true });
+ };
+
+ closeCreatePortfolioForm = () => {
+ this.setState({ createPortfolio: false });
+ };
+
+ handleNewPortfolioClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.openCreatePortfolioForm();
+ };
+
+ handleCreatePortfolio = ({ key, qualifier }: { key: string; qualifier: string }) => {
+ this.closeCreatePortfolioForm();
+ this.props.router.push(getPortfolioAdminUrl(key, qualifier));
+ };
+
+ renderCreateProject() {
+ const { currentUser } = this.props;
+ if (!hasGlobalPermission(currentUser, 'provisioning')) {
+ return null;
+ }
return (
- <Dropdown
- overlay={
- <ul className="menu">
- <li>
- <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
- {translate('provisioning.create_new_project')}
- </a>
- </li>
- <li className="divider" />
- <li>
- <Link className="js-new-organization" to="/create-organization">
- {translate('my_account.create_new_organization')}
- </Link>
- </li>
- </ul>
- }
- tagName="li">
- <a
- className="navbar-plus"
- href="#"
- title={translate('my_account.create_new_project_or_organization')}>
- <PlusIcon />
+ <li>
+ <a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
+ {translate('provisioning.create_new_project')}
</a>
- </Dropdown>
+ </li>
+ );
+ }
+
+ renderCreateOrganization() {
+ if (!isSonarCloud()) {
+ 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 canCreatePortfolio = hasGlobalPermission(currentUser, 'portfoliocreator');
+
+ let defaultQualifier: string | undefined;
+ if (!canCreateApplication) {
+ defaultQualifier = 'VW';
+ } else if (!canCreatePortfolio) {
+ defaultQualifier = 'APP';
+ }
+
+ return (
+ <>
+ <Dropdown
+ overlay={
+ <ul className="menu">
+ {this.renderCreateProject()}
+ {this.renderCreateOrganization()}
+ {this.renderCreatePortfolio(
+ canCreateApplication || canCreatePortfolio,
+ defaultQualifier
+ )}
+ </ul>
+ }
+ tagName="li">
+ <a
+ className="navbar-plus"
+ href="#"
+ title={
+ isSonarCloud()
+ ? translate('my_account.create_new_project_or_organization')
+ : translate('my_account.create_new_project_portfolio_or_application')
+ }>
+ <PlusIcon />
+ </a>
+ </Dropdown>
+ {this.state.governanceReady &&
+ this.state.createPortfolio && (
+ <CreateFormShim
+ onClose={this.closeCreatePortfolioForm}
+ onCreate={this.handleCreatePortfolio}
+ defaultQualifier={defaultQualifier}
+ />
+ )}
+ </>
);
}
}
+
+export default withRouter(GlobalNavPlus);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
index 88aaf4f172d..9305f1ddcbb 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx
@@ -18,23 +18,84 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { shallow } from 'enzyme';
-import GlobalNavPlus from '../GlobalNavPlus';
-import { click } from '../../../../../helpers/testUtils';
+import { shallow, ShallowWrapper } from 'enzyme';
+import { GlobalNavPlus } from '../GlobalNavPlus';
+import { isSonarCloud } from '../../../../../helpers/system';
+import { click, mockRouter } from '../../../../../helpers/testUtils';
+
+jest.mock('../../../../../helpers/system', () => ({
+ isSonarCloud: jest.fn()
+}));
+
+beforeEach(() => {
+ (isSonarCloud as jest.Mock).mockReturnValue(false);
+});
it('render', () => {
- const wrapper = shallow(<GlobalNavPlus openProjectOnboarding={jest.fn()} />);
- expect(wrapper.is('Dropdown')).toBe(true);
+ const wrapper = getWrapper();
expect(wrapper.find('Dropdown')).toMatchSnapshot();
});
it('opens onboarding', () => {
const openProjectOnboarding = jest.fn();
- const wrapper = shallow(
- shallow(<GlobalNavPlus openProjectOnboarding={openProjectOnboarding} />)
- .find('Dropdown')
- .prop('overlay')
- );
+ const wrapper = getOverlayWrapper(getWrapper({ openProjectOnboarding }));
click(wrapper.find('.js-new-project'));
expect(openProjectOnboarding).toBeCalled();
});
+
+it('should display create new project link when user has permission only', () => {
+ expect(
+ getOverlayWrapper(getWrapper({}, []))
+ .find('.js-new-project')
+ .exists()
+ ).toBe(false);
+});
+
+it('should display create new organization on SonarCloud only', () => {
+ (isSonarCloud as jest.Mock).mockReturnValue(true);
+ expect(getOverlayWrapper(getWrapper())).toMatchSnapshot();
+});
+
+it('should display create portfolio and application', () => {
+ checkOpenCreatePortfolio(['applicationcreator', 'portfoliocreator'], undefined);
+});
+
+it('should display create portfolio', () => {
+ checkOpenCreatePortfolio(['portfoliocreator'], 'VW');
+});
+
+it('should display create application', () => {
+ checkOpenCreatePortfolio(['applicationcreator'], 'APP');
+});
+
+function getWrapper(props = {}, globalPermissions?: string[]) {
+ return shallow(
+ <GlobalNavPlus
+ appState={{ qualifiers: [] }}
+ currentUser={{
+ isLoggedIn: true,
+ permissions: { global: globalPermissions || ['provisioning'] }
+ }}
+ openProjectOnboarding={jest.fn()}
+ // @ts-ignore avoid passing everything from WithRouterProps
+ 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);
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
index 6598367a9cc..62c2757d57b 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
@@ -39,13 +39,7 @@ exports[`should render for SonarCloud 1`] = `
}
/>
<EmbedDocsPopupHelper
- currentUser={
- Object {
- "isLoggedIn": false,
- }
- }
suggestions={Array []}
- tooltip={false}
/>
<withRouter(Search)
appState={
@@ -119,13 +113,7 @@ exports[`should render for SonarQube 1`] = `
className="global-navbar-menu global-navbar-menu-right"
>
<EmbedDocsPopupHelper
- currentUser={
- Object {
- "isLoggedIn": false,
- }
- }
suggestions={Array []}
- tooltip={true}
/>
<withRouter(Search)
appState={
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
index a051a721d21..a54b7dfefcb 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
@@ -15,19 +15,6 @@ exports[`render 1`] = `
provisioning.create_new_project
</a>
</li>
- <li
- className="divider"
- />
- <li>
- <Link
- className="js-new-organization"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/create-organization"
- >
- my_account.create_new_organization
- </Link>
- </li>
</ul>
}
tagName="li"
@@ -35,9 +22,65 @@ exports[`render 1`] = `
<a
className="navbar-plus"
href="#"
- title="my_account.create_new_project_or_organization"
+ title="my_account.create_new_project_portfolio_or_application"
>
<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"
+ href="#"
+ onClick={[Function]}
+ >
+ provisioning.create_new_project
+ </a>
+ </li>
+ <li>
+ <Link
+ className="js-new-organization"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/create-organization"
+ >
+ my_account.create_new_organization
+ </Link>
+ </li>
+</ul>
+`;
+
+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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx
new file mode 100644
index 00000000000..0bf661035b6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/CreateFormShim.tsx
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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';
+
+interface Props {
+ defaultQualifier?: string;
+ onClose: () => void;
+ onCreate: (portfolio: { key: string; qualifier: string }) => void;
+}
+
+export default class CreateFormShim extends React.Component<Props> {
+ render() {
+ const { CreateForm } = (window as any).SonarGovernance;
+ return <CreateForm {...this.props} />;
+ }
+}
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index 9d3959260bf..cc207fd5c2b 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -57,6 +57,10 @@ export function getPortfolioUrl(key: string): Location {
return { pathname: '/portfolio', query: { id: key } };
}
+export function getPortfolioAdminUrl(key: string, qualifier: string) {
+ return { pathname: '/project/admin/extension/governance/console', query: { id: key, qualifier } };
+}
+
export function getComponentBackgroundTaskUrl(componentKey: string, status?: string): Location {
return { pathname: '/project/background_tasks', query: { id: componentKey, status } };
}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index aaac8fa91d7..66508b5d588 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -1492,8 +1492,12 @@ my_account.create_organization=Create Organization
my_account.search_project=Search Project
my_account.set_notifications_for=Set notifications for
my_account.analyze_new_project=Analyze new project
+my_account.create_new_portfolio_application=Create new portfolio / application
+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=Create new project or organization
+my_account.create_new_project_portfolio_or_application=Create new project, portfolio or application
#------------------------------------------------------------------------------