aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy <47277647+jeremy-davis-sonarsource@users.noreply.github.com>2019-03-04 15:19:47 +0100
committersonartech <sonartech@sonarsource.com>2019-03-06 11:30:44 +0100
commitb41f2655daa7d58a272f0f98ac1ec16ff10f6e24 (patch)
tree6c4a2db000811c72ac74a6069a139e91de887313
parent052e3b08498bb3c42aaabaaeea2325dfb03e2bac (diff)
downloadsonarqube-b41f2655daa7d58a272f0f98ac1ec16ff10f6e24.tar.gz
sonarqube-b41f2655daa7d58a272f0f98ac1ec16ff10f6e24.zip
SONARCLOUD-343 improve existing org alm binding
-rw-r--r--server/sonar-web/public/images/sonarcloud/bitbucket-unbound.svg12
-rw-r--r--server/sonar-web/public/images/sonarcloud/github-unbound.svg7
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/boxed-group.css6
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/utils.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationBind.tsx126
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationBind-test.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEdit-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationBind-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap222
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap27
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx63
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeaderContainer.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx40
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap77
-rw-r--r--server/sonar-web/src/main/js/components/nav/ContextNavBar.css3
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts17
-rw-r--r--server/sonar-web/src/main/js/helpers/almIntegrations.ts6
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties17
26 files changed, 752 insertions, 310 deletions
diff --git a/server/sonar-web/public/images/sonarcloud/bitbucket-unbound.svg b/server/sonar-web/public/images/sonarcloud/bitbucket-unbound.svg
new file mode 100644
index 00000000000..c0bca9debf3
--- /dev/null
+++ b/server/sonar-web/public/images/sonarcloud/bitbucket-unbound.svg
@@ -0,0 +1,12 @@
+<svg width="59" height="54" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <linearGradient x1="97.494%" y1="25.5%" x2="46.9%" y2="78.8%" id="a">
+ <stop stop-color="#B5B5B5" offset="0%"/>
+ <stop stop-color="#DDD" offset="100%"/>
+ </linearGradient>
+ </defs>
+ <g fill-rule="nonzero" fill="none">
+ <path d="M1.973.987A1.872 1.872 0 0 0 .102 3.159l7.947 48.253a2.547 2.547 0 0 0 2.49 2.125h38.133a1.872 1.872 0 0 0 1.872-1.573l7.949-48.8A1.872 1.872 0 0 0 56.621.992L1.973.987zm33.47 34.875H23.272l-3.3-17.217h18.42l-2.949 17.217z" fill="#DDD"/>
+ <path d="M55.965 18.644H38.392l-2.949 17.217H23.272L8.901 52.924a2.537 2.537 0 0 0 1.638.618h38.142a1.872 1.872 0 0 0 1.872-1.573l5.412-33.325z" fill="url(#a)"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/server/sonar-web/public/images/sonarcloud/github-unbound.svg b/server/sonar-web/public/images/sonarcloud/github-unbound.svg
new file mode 100644
index 00000000000..17307828edc
--- /dev/null
+++ b/server/sonar-web/public/images/sonarcloud/github-unbound.svg
@@ -0,0 +1,7 @@
+<svg width="14" height="14" xmlns="http://www.w3.org/2000/svg">
+ <g fill="none" fill-rule="evenodd">
+ <path d="M-1-1h16v16H-1z"/>
+ <path d="M13.061 3.574A7.06 7.06 0 0 0 10.514.962 6.719 6.719 0 0 0 7 0a6.72 6.72 0 0 0-3.514.962A7.059 7.059 0 0 0 .94 3.574 7.155 7.155 0 0 0 0 7.175c0 1.564.445 2.97 1.335 4.219.89 1.248 2.04 2.113 3.45 2.592.164.031.286.01.365-.065a.37.37 0 0 0 .118-.28l-.005-.505a85.532 85.532 0 0 1-.004-.831l-.21.037a2.61 2.61 0 0 1-.506.033 3.763 3.763 0 0 1-.633-.066 1.399 1.399 0 0 1-.61-.28 1.185 1.185 0 0 1-.402-.574l-.09-.215a2.346 2.346 0 0 0-.288-.477c-.13-.174-.263-.292-.396-.355l-.064-.047a.676.676 0 0 1-.119-.112.514.514 0 0 1-.082-.13c-.018-.044-.003-.08.046-.108.049-.028.137-.042.264-.042l.182.028c.122.025.272.1.452.224.179.125.326.287.442.486.14.255.308.45.505.584.198.134.397.2.597.2.2 0 .374-.015.52-.046.146-.031.282-.078.41-.14.055-.418.204-.738.447-.962a6.103 6.103 0 0 1-.935-.169 3.67 3.67 0 0 1-.856-.364 2.47 2.47 0 0 1-.734-.626c-.194-.25-.354-.576-.478-.981A4.774 4.774 0 0 1 2.534 6.8c0-.753.24-1.395.72-1.924-.225-.567-.204-1.202.064-1.906.176-.056.437-.014.783.126.347.14.6.26.761.36.162.1.29.183.388.252A6.324 6.324 0 0 1 7 3.466c.601 0 1.185.081 1.75.243l.346-.224c.237-.15.517-.287.839-.411.322-.125.568-.16.738-.103.274.704.298 1.339.073 1.906.48.53.72 1.17.72 1.924 0 .53-.062.998-.187 1.407-.124.408-.285.734-.483.98a2.563 2.563 0 0 1-.738.622c-.295.168-.58.29-.857.364a6.097 6.097 0 0 1-.934.169c.316.28.474.722.474 1.326v1.971c0 .112.038.206.114.28.076.075.196.097.36.066 1.41-.48 2.56-1.344 3.45-2.593C13.555 10.145 14 8.74 14 7.175a7.16 7.16 0 0 0-.939-3.601z" fill="#DDD" fill-rule="nonzero"/>
+ </g>
+</svg>
+
diff --git a/server/sonar-web/src/main/js/app/styles/components/boxed-group.css b/server/sonar-web/src/main/js/app/styles/components/boxed-group.css
index 6912e432353..119c9a9cf40 100644
--- a/server/sonar-web/src/main/js/app/styles/components/boxed-group.css
+++ b/server/sonar-web/src/main/js/app/styles/components/boxed-group.css
@@ -35,6 +35,12 @@
padding: calc(2 * var(--gridSize)) 20px 0;
}
+.boxed-group > h2.boxed-title {
+ padding: 0 0 8px;
+ margin: 0;
+ font-weight: 600;
+}
+
.boxed-group hr {
height: 0;
border-top: 1px solid var(--gray94);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
index 0e441863c12..4d28a6aed92 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
@@ -31,7 +31,9 @@ import {
serializeQuery,
Query,
ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP,
- Step
+ Step,
+ BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP,
+ BIND_ORGANIZATION_KEY
} from './utils';
import AlmApplicationInstalling from './AlmApplicationInstalling';
import AutoOrganizationCreate from './AutoOrganizationCreate';
@@ -47,7 +49,8 @@ import {
getAlmAppInfo,
getAlmOrganization,
GetAlmOrganizationResponse,
- listUnboundApplications
+ listUnboundApplications,
+ bindAlmOrganization
} from '../../../api/alm-integration';
import { getSubscriptionPlans } from '../../../api/billing';
import * as api from '../../../api/organizations';
@@ -62,6 +65,7 @@ import { get, remove } from '../../../helpers/storage';
import { slugify } from '../../../helpers/strings';
import { getOrganizationUrl } from '../../../helpers/urls';
import { skipOnboarding } from '../../../store/users';
+import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage';
import '../../tutorials/styles.css'; // TODO remove me
interface Props {
@@ -82,6 +86,7 @@ interface State {
almOrganization?: T.AlmOrganization;
almOrgLoading: boolean;
almUnboundApplications: T.AlmUnboundApplication[];
+ bindingExistingOrg: boolean;
boundOrganization?: T.OrganizationBase;
loading: boolean;
organization?: T.Organization;
@@ -102,6 +107,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
state: State = {
almOrgLoading: false,
almUnboundApplications: [],
+ bindingExistingOrg: false,
loading: true,
step: Step.OrganizationDetails
};
@@ -109,18 +115,29 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
componentDidMount() {
this.mounted = true;
addWhitePageClass();
- const initRequests = [this.fetchSubscriptionPlans()];
- if (hasAdvancedALMIntegration(this.props.currentUser)) {
- initRequests.push(this.fetchAlmApplication());
- const query = parseQuery(this.props.location.query);
- if (query.almInstallId) {
- this.fetchAlmOrganization(query.almInstallId);
- } else {
- initRequests.push(this.fetchAlmUnboundApplications());
+ const query = parseQuery(this.props.location.query);
+
+ //highjack the process for the organization settings
+ if (
+ hasAdvancedALMIntegration(this.props.currentUser) &&
+ query.almInstallId &&
+ this.isStoredTimestampValid(BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP)
+ ) {
+ this.bindAndRedirectToOrganizationSettings(query.almInstallId);
+ } else {
+ const initRequests = [this.fetchSubscriptionPlans()];
+ if (hasAdvancedALMIntegration(this.props.currentUser)) {
+ initRequests.push(this.fetchAlmApplication());
+
+ if (query.almInstallId) {
+ this.fetchAlmOrganization(query.almInstallId);
+ } else {
+ initRequests.push(this.fetchAlmUnboundApplications());
+ }
}
+ Promise.all(initRequests).then(this.stopLoading, this.stopLoading);
}
- Promise.all(initRequests).then(this.stopLoading, this.stopLoading);
}
componentDidUpdate(prevProps: WithRouterProps) {
@@ -249,6 +266,36 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
this.updateUrlState({ tab });
};
+ bindAndRedirectToOrganizationSettings(installationId: string) {
+ const organizationKey = get(BIND_ORGANIZATION_KEY) || '';
+ remove(BIND_ORGANIZATION_KEY);
+
+ this.setState({ bindingExistingOrg: true });
+
+ bindAlmOrganization({
+ installationId,
+ organization: organizationKey
+ }).then(
+ () => {
+ this.props.router.push({
+ pathname: `/organizations/${organizationKey}`
+ });
+ addGlobalSuccessMessage(translate('organization.bind.success'));
+ },
+ () => {}
+ );
+ }
+
+ getHeader = (bindingExistingOrg: boolean, importPersonalOrg: boolean) => {
+ if (bindingExistingOrg) {
+ return translate('onboarding.binding_organization');
+ } else if (importPersonalOrg) {
+ return translate('onboarding.import_organization.personal.page.header');
+ } else {
+ return translate('onboarding.create_organization.page.header');
+ }
+ };
+
setValidOrgKey = (almOrganization: T.AlmOrganization) => {
const key = slugify(almOrganization.key);
const keys = [key, ...times(9, i => `${key}-${i + 1}`)];
@@ -397,13 +444,12 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
return <AlmApplicationInstalling almKey={query.almKey} />;
}
- const { almOrganization, subscriptionPlans } = this.state;
+ const { almOrganization, bindingExistingOrg, subscriptionPlans } = this.state;
const importPersonalOrg = isPersonal(almOrganization)
? this.props.userOrganizations.find(o => o.key === currentUser.personalOrganization)
: undefined;
- const header = importPersonalOrg
- ? translate('onboarding.import_organization.personal.page.header')
- : translate('onboarding.create_organization.page.header');
+ const header = this.getHeader(bindingExistingOrg, !!importPersonalOrg);
+
const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
return (
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
index fc80778c403..cdc8020444b 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
@@ -20,9 +20,10 @@
import * as React from 'react';
import { times } from 'lodash';
import { Location } from 'history';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
import { CreateOrganization } from '../CreateOrganization';
import {
+ bindAlmOrganization,
getAlmAppInfo,
getAlmOrganization,
listUnboundApplications
@@ -35,7 +36,8 @@ import {
mockOrganizationWithAdminActions,
mockOrganizationWithAlm,
mockAlmOrganization,
- mockCurrentUser
+ mockCurrentUser,
+ mockLocation
} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
@@ -67,7 +69,8 @@ jest.mock('../../../../api/alm-integration', () => ({
url: 'https://www.sonarsource.com'
}
}),
- listUnboundApplications: jest.fn().mockResolvedValue([])
+ listUnboundApplications: jest.fn().mockResolvedValue([]),
+ bindAlmOrganization: jest.fn().mockResolvedValue({})
}));
jest.mock('../../../../api/organizations', () => ({
@@ -246,7 +249,9 @@ it('should display AutoOrganizationCreate with already bound organization', asyn
almOrganization: { ...fooBarAlmOrganization, personal: false },
boundOrganization
});
- (get as jest.Mock<any>).mockReturnValueOnce(Date.now().toString());
+ (get as jest.Mock<any>)
+ .mockReturnValueOnce(undefined) // For BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP
+ .mockReturnValueOnce(Date.now().toString()); // For ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP
const push = jest.fn();
const wrapper = shallowRender({
currentUser: { ...user, externalProvider: 'github' },
@@ -294,18 +299,50 @@ it('should cancel imports', async () => {
const wrapper = shallowRender({ router: mockRouter({ push }) });
await waitAndUpdate(wrapper);
wrapper.instance().handleCancelImport();
- expect(push).toBeCalledWith({ query: {} });
+ expect(push).toBeCalledWith({ pathname: '/path', query: {}, state: {} });
});
+it('should bind org and redirect to org home when coming from org binding', async () => {
+ const installation_id = '5328';
+ const orgKey = 'org4test';
+ const push = jest.fn();
+
+ (get as jest.Mock<any>)
+ .mockReturnValueOnce(Date.now().toString()) // For BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP
+ .mockReturnValueOnce(orgKey); // For BIND_ORGANIZATION_KEY
+
+ const wrapper = mountRender({
+ currentUser: mockCurrentUser({ ...user, externalProvider: 'github' }),
+ location: mockLocation({ query: { installation_id } }),
+ router: mockRouter({ push })
+ });
+ await waitAndUpdate(wrapper);
+
+ expect(bindAlmOrganization).toBeCalled();
+ expect(getAlmOrganization).not.toBeCalled();
+ expect(push).toBeCalledWith({
+ pathname: `/organizations/${orgKey}`
+ });
+});
+
+function mountRender(props: Partial<CreateOrganization['props']> = {}) {
+ return mount<CreateOrganization>(createComponent(props));
+}
+
function shallowRender(props: Partial<CreateOrganization['props']> = {}) {
- return shallow<CreateOrganization>(
+ return shallow<CreateOrganization>(createComponent(props));
+}
+
+function createComponent(props: Partial<CreateOrganization['props']> = {}) {
+ return (
<CreateOrganization
createOrganization={jest.fn()}
currentUser={user}
deleteOrganization={jest.fn()}
- // @ts-ignore avoid passing everything from WithRouterProps
- location={{}}
+ location={mockLocation()}
+ params={{}}
router={mockRouter()}
+ routes={[]}
skipOnboarding={jest.fn()}
updateOrganization={jest.fn()}
userOrganizations={[
diff --git a/server/sonar-web/src/main/js/apps/create/organization/utils.ts b/server/sonar-web/src/main/js/apps/create/organization/utils.ts
index b6d88841e2b..0d9485bba1a 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/utils.ts
+++ b/server/sonar-web/src/main/js/apps/create/organization/utils.ts
@@ -35,6 +35,10 @@ export const ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP =
export const ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP =
'sonarcloud.import_org.redirect_to_projects';
+export const BIND_ORGANIZATION_KEY = 'sonarcloud.bind_org.key';
+
+export const BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP = 'sonarcloud.bind_org.redirect_to_org';
+
export enum Step {
OrganizationDetails,
Plan
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationBind.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationBind.tsx
new file mode 100644
index 00000000000..a46360015c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationBind.tsx
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 {
+ BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP,
+ BIND_ORGANIZATION_KEY
+} from '../../create/organization/utils';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import { getAlmAppInfo } from '../../../api/alm-integration';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+import { save } from '../../../helpers/storage';
+import { getBaseUrl } from '../../../helpers/urls';
+
+interface Props {
+ currentUser: T.LoggedInUser;
+ organization: T.Organization;
+}
+
+interface State {
+ almApplication?: T.AlmApplication;
+}
+
+export default class OrganizationBind extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {};
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchAlmApplication();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchAlmApplication = () => {
+ return getAlmAppInfo().then(({ application }) => {
+ if (this.mounted) {
+ this.setState({ almApplication: application });
+ }
+ });
+ };
+
+ handleInstallAppClick = () => {
+ save(BIND_ORGANIZATION_KEY, this.props.organization.key);
+ save(BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP, Date.now().toString());
+ };
+
+ render() {
+ const { currentUser, organization } = this.props;
+
+ const { almApplication } = this.state;
+
+ const almKey = sanitizeAlmId(currentUser.externalProvider || '');
+ const orgAlmKey = organization.alm ? sanitizeAlmId(organization.alm.key) : '';
+ return (
+ <div className="boxed-group boxed-group-inner">
+ <h2 className="boxed-title">
+ {translateWithParameters('organization.bind_to_x', translate(almKey))}
+ </h2>
+ {organization.alm ? (
+ <>
+ <span>{translate('organization.bound')}</span>
+ <a
+ className="link-no-underline big-spacer-left"
+ href={organization.alm.url}
+ rel="noopener noreferrer"
+ target="_blank">
+ <img
+ alt={translate(orgAlmKey)}
+ className="text-text-top little-spacer-right"
+ height={16}
+ src={`${getBaseUrl()}/images/sonarcloud/${orgAlmKey}.svg`}
+ width={16}
+ />
+ {translateWithParameters('organization.see_on_x', translate(orgAlmKey))}
+ </a>
+ </>
+ ) : (
+ <>
+ <p className="spacer-bottom">
+ {translateWithParameters('organization.binding_with_x_easy_sync', translate(almKey))}
+ </p>
+ <p className="big-spacer-bottom">
+ {translateWithParameters(
+ 'organization.app_will_be_installed_on_x',
+ translate(almKey)
+ )}
+ </p>
+ {almApplication && (
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={almApplication}
+ onClick={this.handleInstallAppClick}
+ small={true}
+ url={almApplication.installationUrl}>
+ {translate(
+ 'onboarding.import_organization.choose_the_organization_button',
+ almApplication.key
+ )}
+ </IdentityProviderLink>
+ )}
+ </>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx
index 6954e3bf7e7..ab685c20442 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import ConfirmButton from '../../../components/controls/ConfirmButton';
import InstanceMessage from '../../../components/common/InstanceMessage';
@@ -111,59 +110,53 @@ export class OrganizationDelete extends React.PureComponent<Props, State> {
render() {
const { hasPaidPlan } = this.state;
- const title = translate('organization.delete');
return (
- <>
- <Helmet title={title} />
- <div className="page page-limited">
- <header className="page-header">
- <h1 className="page-title">{title}</h1>
- <div className="page-description">
- <InstanceMessage message={translate('organization.delete.description')} />
- </div>
- </header>
- <ConfirmButton
- confirmButtonText={translate('delete')}
- confirmDisable={!this.isVerified()}
- isDestructive={true}
- modalBody={
- <div>
- {hasPaidPlan && (
- <Alert variant="warning">
- {translate('organization.delete.sonarcloud.paid_plan_info')}
- </Alert>
- )}
- <p>{translate('organization.delete.question')}</p>
- <div className="spacer-top">
- <label htmlFor="downgrade-organization-name">
- {translate('billing.downgrade.modal.type_to_proceed')}
- </label>
- <div className="little-spacer-top">
- <input
- autoFocus={true}
- className="input-super-large"
- id="downgrade-organization-name"
- onChange={this.handleInput}
- type="text"
- value={this.state.verify}
- />
- </div>
+ <div className="boxed-group boxed-group-inner">
+ <h2 className="boxed-title">{translate('organization.delete')}</h2>
+ <p className="big-spacer-bottom width-50">
+ <InstanceMessage message={translate('organization.delete.description')} />
+ </p>
+ <ConfirmButton
+ confirmButtonText={translate('delete')}
+ confirmDisable={!this.isVerified()}
+ isDestructive={true}
+ modalBody={
+ <div>
+ {hasPaidPlan && (
+ <Alert variant="warning">
+ {translate('organization.delete.sonarcloud.paid_plan_info')}
+ </Alert>
+ )}
+ <p>{translate('organization.delete.question')}</p>
+ <div className="spacer-top">
+ <label htmlFor="downgrade-organization-name">
+ {translate('billing.downgrade.modal.type_to_proceed')}
+ </label>
+ <div className="little-spacer-top">
+ <input
+ autoFocus={true}
+ className="input-super-large"
+ id="downgrade-organization-name"
+ onChange={this.handleInput}
+ type="text"
+ value={this.state.verify}
+ />
</div>
</div>
- }
- modalHeader={translateWithParameters(
- 'organization.delete_x',
- this.props.organization.name
- )}
- onConfirm={this.onDelete}>
- {({ onClick }) => (
- <Button className="js-custom-measure-delete button-red" onClick={onClick}>
- {translate('delete')}
- </Button>
- )}
- </ConfirmButton>
- </div>
- </>
+ </div>
+ }
+ modalHeader={translateWithParameters(
+ 'organization.delete_x',
+ this.props.organization.name
+ )}
+ onConfirm={this.onDelete}>
+ {({ onClick }) => (
+ <Button className="js-custom-measure-delete button-red" onClick={onClick}>
+ {translate('delete')}
+ </Button>
+ )}
+ </ConfirmButton>
+ </div>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx
index 4d151adf2ab..77942da7b55 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx
@@ -21,9 +21,13 @@ import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
+import OrganizationBind from './OrganizationBind';
+import OrganizationDelete from './OrganizationDelete';
+import { updateOrganization } from '../actions';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { SubmitButton } from '../../../components/ui/buttons';
-import { updateOrganization } from '../actions';
+import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
import { translate } from '../../../helpers/l10n';
interface DispatchProps {
@@ -31,6 +35,7 @@ interface DispatchProps {
}
interface OwnProps {
+ currentUser: T.LoggedInUser;
organization: T.Organization;
}
@@ -100,7 +105,12 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
};
render() {
- const title = translate('organization.edit');
+ const { currentUser, organization } = this.props;
+ const title = translate('organization.settings');
+
+ const showBinding = hasAdvancedALMIntegration(currentUser);
+ const showDelete = organization.actions && organization.actions.delete;
+
return (
<div className="page page-limited">
<Helmet title={title} />
@@ -110,6 +120,7 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
</header>
<div className="boxed-group boxed-group-inner">
+ <h2 className="boxed-title">{translate('organization.details')}</h2>
<form onSubmit={this.handleSubmit}>
<div className="form-field">
<label htmlFor="organization-name">
@@ -198,6 +209,10 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
{this.state.loading && <i className="spinner spacer-left" />}
</form>
</div>
+
+ {showBinding && <OrganizationBind currentUser={currentUser} organization={organization} />}
+
+ {showDelete && <OrganizationDelete organization={organization} />}
</div>
);
}
@@ -208,4 +223,4 @@ const mapDispatchToProps = { updateOrganization: updateOrganization as any };
export default connect(
null,
mapDispatchToProps
-)(OrganizationEdit);
+)(whenLoggedIn(OrganizationEdit));
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationBind-test.tsx
new file mode 100644
index 00000000000..079de73c67e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationBind-test.tsx
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import OrganizationBind from '../OrganizationBind';
+import {
+ BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP,
+ BIND_ORGANIZATION_KEY
+} from '../../../create/organization/utils';
+import { getAlmAppInfo } from '../../../../api/alm-integration';
+import { save } from '../../../../helpers/storage';
+import {
+ mockAlmApplication,
+ mockCurrentUser,
+ mockOrganization
+} from '../../../../helpers/testMocks';
+
+jest.mock('../../../../api/alm-integration', () => ({
+ getAlmAppInfo: jest.fn(() => Promise.resolve({ application: mockAlmApplication() }))
+}));
+jest.mock('../../../../helpers/storage', () => ({
+ save: jest.fn()
+}));
+
+jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));
+
+beforeEach(() => {
+ (getAlmAppInfo as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should save state when handling Install App click', () => {
+ const orgKey = '56346';
+ shallowRender({ organization: mockOrganization({ key: orgKey }) })
+ .instance()
+ .handleInstallAppClick();
+
+ expect(save).toBeCalledTimes(2);
+ expect(save).nthCalledWith(1, BIND_ORGANIZATION_KEY, orgKey);
+ const secondCallArguments = (save as jest.Mock<any>).mock.calls[1];
+ expect(secondCallArguments[0]).toBe(BIND_ORGANIZATION_REDIRECT_TO_ORG_TIMESTAMP);
+});
+
+function shallowRender(props: Partial<OrganizationBind['props']> = {}) {
+ return shallow<OrganizationBind>(
+ <OrganizationBind
+ currentUser={mockCurrentUser()}
+ organization={mockOrganization()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEdit-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEdit-test.tsx
index 1e320c7865b..1daa8265779 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEdit-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEdit-test.tsx
@@ -20,11 +20,16 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { OrganizationEdit } from '../OrganizationEdit';
+import { mockCurrentUser } from '../../../../helpers/testMocks';
it('smoke test', () => {
const organization = { key: 'foo', name: 'Foo' };
const wrapper = shallow(
- <OrganizationEdit organization={organization} updateOrganization={jest.fn()} />
+ <OrganizationEdit
+ currentUser={mockCurrentUser()}
+ organization={organization}
+ updateOrganization={jest.fn()}
+ />
);
expect(wrapper).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationBind-test.tsx.snap
new file mode 100644
index 00000000000..69821637e05
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationBind-test.tsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="boxed-group boxed-group-inner"
+>
+ <h2
+ className="boxed-title"
+ >
+ organization.bind_to_x.
+ </h2>
+ <p
+ className="spacer-bottom"
+ >
+ organization.binding_with_x_easy_sync.
+ </p>
+ <p
+ className="big-spacer-bottom"
+ >
+ organization.app_will_be_installed_on_x.
+ </p>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap
index 43b66bfaea3..c2dee60e8f3 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap
@@ -1,140 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should show a info message for paying organization 1`] = `
-<Fragment>
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="organization.delete"
- />
- <div
- className="page page-limited"
+<div
+ className="boxed-group boxed-group-inner"
+>
+ <h2
+ className="boxed-title"
>
- <header
- className="page-header"
- >
- <h1
- className="page-title"
- >
- organization.delete
- </h1>
- <div
- className="page-description"
- >
- <InstanceMessage
- message="organization.delete.description"
- />
- </div>
- </header>
- <ConfirmButton
- confirmButtonText="delete"
- confirmDisable={true}
- isDestructive={true}
- modalBody={
- <div>
- <Alert
- variant="warning"
+ organization.delete
+ </h2>
+ <p
+ className="big-spacer-bottom width-50"
+ >
+ <InstanceMessage
+ message="organization.delete.description"
+ />
+ </p>
+ <ConfirmButton
+ confirmButtonText="delete"
+ confirmDisable={true}
+ isDestructive={true}
+ modalBody={
+ <div>
+ <Alert
+ variant="warning"
+ >
+ organization.delete.sonarcloud.paid_plan_info
+ </Alert>
+ <p>
+ organization.delete.question
+ </p>
+ <div
+ className="spacer-top"
+ >
+ <label
+ htmlFor="downgrade-organization-name"
>
- organization.delete.sonarcloud.paid_plan_info
- </Alert>
- <p>
- organization.delete.question
- </p>
+ billing.downgrade.modal.type_to_proceed
+ </label>
<div
- className="spacer-top"
+ className="little-spacer-top"
>
- <label
- htmlFor="downgrade-organization-name"
- >
- billing.downgrade.modal.type_to_proceed
- </label>
- <div
- className="little-spacer-top"
- >
- <input
- autoFocus={true}
- className="input-super-large"
- id="downgrade-organization-name"
- onChange={[Function]}
- type="text"
- value=""
- />
- </div>
+ <input
+ autoFocus={true}
+ className="input-super-large"
+ id="downgrade-organization-name"
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
</div>
</div>
- }
- modalHeader="organization.delete_x.Foo"
- onConfirm={[Function]}
- >
- <Component />
- </ConfirmButton>
- </div>
-</Fragment>
+ </div>
+ }
+ modalHeader="organization.delete_x.Foo"
+ onConfirm={[Function]}
+ >
+ <Component />
+ </ConfirmButton>
+</div>
`;
exports[`smoke test 1`] = `
-<Fragment>
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="organization.delete"
- />
- <div
- className="page page-limited"
+<div
+ className="boxed-group boxed-group-inner"
+>
+ <h2
+ className="boxed-title"
>
- <header
- className="page-header"
- >
- <h1
- className="page-title"
- >
- organization.delete
- </h1>
- <div
- className="page-description"
- >
- <InstanceMessage
- message="organization.delete.description"
- />
- </div>
- </header>
- <ConfirmButton
- confirmButtonText="delete"
- confirmDisable={true}
- isDestructive={true}
- modalBody={
- <div>
- <p>
- organization.delete.question
- </p>
+ organization.delete
+ </h2>
+ <p
+ className="big-spacer-bottom width-50"
+ >
+ <InstanceMessage
+ message="organization.delete.description"
+ />
+ </p>
+ <ConfirmButton
+ confirmButtonText="delete"
+ confirmDisable={true}
+ isDestructive={true}
+ modalBody={
+ <div>
+ <p>
+ organization.delete.question
+ </p>
+ <div
+ className="spacer-top"
+ >
+ <label
+ htmlFor="downgrade-organization-name"
+ >
+ billing.downgrade.modal.type_to_proceed
+ </label>
<div
- className="spacer-top"
+ className="little-spacer-top"
>
- <label
- htmlFor="downgrade-organization-name"
- >
- billing.downgrade.modal.type_to_proceed
- </label>
- <div
- className="little-spacer-top"
- >
- <input
- autoFocus={true}
- className="input-super-large"
- id="downgrade-organization-name"
- onChange={[Function]}
- type="text"
- value=""
- />
- </div>
+ <input
+ autoFocus={true}
+ className="input-super-large"
+ id="downgrade-organization-name"
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
</div>
</div>
- }
- modalHeader="organization.delete_x.Foo"
- onConfirm={[Function]}
- >
- <Component />
- </ConfirmButton>
- </div>
-</Fragment>
+ </div>
+ }
+ modalHeader="organization.delete_x.Foo"
+ onConfirm={[Function]}
+ >
+ <Component />
+ </ConfirmButton>
+</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap
index e948f1810c5..b80d299eb16 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap
@@ -7,7 +7,7 @@ exports[`smoke test 1`] = `
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
- title="organization.edit"
+ title="organization.settings"
/>
<header
className="page-header"
@@ -15,12 +15,17 @@ exports[`smoke test 1`] = `
<h1
className="page-title"
>
- organization.edit
+ organization.settings
</h1>
</header>
<div
className="boxed-group boxed-group-inner"
>
+ <h2
+ className="boxed-title"
+ >
+ organization.details
+ </h2>
<form
onSubmit={[Function]}
>
@@ -162,7 +167,7 @@ exports[`smoke test 2`] = `
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
- title="organization.edit"
+ title="organization.settings"
/>
<header
className="page-header"
@@ -170,12 +175,17 @@ exports[`smoke test 2`] = `
<h1
className="page-title"
>
- organization.edit
+ organization.settings
</h1>
</header>
<div
className="boxed-group boxed-group-inner"
>
+ <h2
+ className="boxed-title"
+ >
+ organization.details
+ </h2>
<form
onSubmit={[Function]}
>
@@ -317,7 +327,7 @@ exports[`smoke test 3`] = `
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
- title="organization.edit"
+ title="organization.settings"
/>
<header
className="page-header"
@@ -325,12 +335,17 @@ exports[`smoke test 3`] = `
<h1
className="page-title"
>
- organization.edit
+ organization.settings
</h1>
</header>
<div
className="boxed-group boxed-group-inner"
>
+ <h2
+ className="boxed-title"
+ >
+ organization.details
+ </h2>
<form
onSubmit={[Function]}
>
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
index 3c7bbdb76a8..e9219a6bffd 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import OrganizationNavigationHeaderContainer from './OrganizationNavigationHeaderContainer';
+import OrganizationNavigationHeader from './OrganizationNavigationHeader';
import OrganizationNavigationMeta from './OrganizationNavigationMeta';
import OrganizationNavigationMenuContainer from './OrganizationNavigationMenuContainer';
import * as theme from '../../../app/theme';
@@ -40,7 +40,11 @@ export default function OrganizationNavigation({
return (
<ContextNavBar height={theme.contextNavHeightRaw} id="context-navigation">
<div className="navbar-context-justified">
- <OrganizationNavigationHeaderContainer organization={organization} />
+ <OrganizationNavigationHeader
+ currentUser={currentUser}
+ organization={organization}
+ organizations={userOrganizations}
+ />
<OrganizationNavigationMeta
currentUser={currentUser}
organization={organization}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
index 63228606f33..f1eb48f7807 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
@@ -32,7 +32,6 @@ interface Props {
const ADMIN_PATHS = [
'edit',
'groups',
- 'delete',
'permissions',
'permission_templates',
'projects_management',
@@ -40,7 +39,7 @@ const ADMIN_PATHS = [
];
export default function OrganizationNavigationAdministration({ location, organization }: Props) {
- const { actions = {}, adminPages = [] } = organization;
+ const { adminPages = [] } = organization;
const adminPathsWithExtensions = adminPages.map(e => `extension/${e.key}`).concat(ADMIN_PATHS);
const adminActive = adminPathsWithExtensions.some(path =>
location.pathname.endsWith(`organizations/${organization.key}/${path}`)
@@ -50,6 +49,11 @@ export default function OrganizationNavigationAdministration({ location, organiz
<Dropdown
overlay={
<ul className="menu">
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/edit`}>
+ {translate('organization.settings')}
+ </Link>
+ </li>
{adminPages.map(extension => (
<li key={extension.key}>
<Link
@@ -88,18 +92,6 @@ export default function OrganizationNavigationAdministration({ location, organiz
{translate('webhooks.page')}
</Link>
</li>
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/edit`}>
- {translate('edit')}
- </Link>
- </li>
- {actions.delete && (
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/delete`}>
- {translate('delete')}
- </Link>
- </li>
- )}
</ul>
}
tagName="li">
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
index 2f7426880c4..28b8d1cb6d9 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
@@ -18,22 +18,63 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { Link } from 'react-router';
import { sortBy } from 'lodash';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import Dropdown from '../../../components/controls/Dropdown';
+import Tooltip from '../../../components/controls/Tooltip';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import OrganizationListItem from '../../../components/ui/OrganizationListItem';
-import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import {
+ sanitizeAlmId,
+ hasAdvancedALMIntegration,
+ getUserAlmKey
+} from '../../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
-interface Props {
+export interface Props {
+ currentUser: T.CurrentUser;
organization: T.Organization;
organizations: T.Organization[];
}
-export default function OrganizationNavigationHeader({ organization, organizations }: Props) {
+export default function OrganizationNavigationHeader({
+ currentUser,
+ organization,
+ organizations
+}: Props) {
const other = organizations.filter(o => o.key !== organization.key);
+ let almKey;
+ let tooltipContent;
+ let tooltipIconSrc;
+ if (organization.alm) {
+ almKey = sanitizeAlmId(organization.alm.key);
+ tooltipContent = (
+ <>
+ <p>{translateWithParameters('organization.bound_to_x', translate(almKey))}</p>
+ <hr className="spacer-top spacer-bottom" />
+ <a href={organization.alm.url} rel="noopener noreferrer" target="_blank">
+ {translateWithParameters('organization.see_on_x', translate(almKey))}
+ </a>
+ </>
+ );
+ tooltipIconSrc = `${getBaseUrl()}/images/sonarcloud/${almKey}.svg`;
+ } else if (hasAdvancedALMIntegration(currentUser)) {
+ almKey = getUserAlmKey(currentUser) || '';
+ tooltipContent = (
+ <>
+ <p>{translateWithParameters('organization.not_bound_to_x', translate(almKey))}</p>
+ <hr className="spacer-top spacer-bottom" />
+ <Link to={`/organizations/${organization.key}/edit`}>
+ {translate('organization.go_to_settings_to_bind')}
+ </Link>
+ </>
+ );
+ tooltipIconSrc = `${getBaseUrl()}/images/sonarcloud/${almKey}-unbound.svg`;
+ }
+
return (
<header className="navbar-context-header">
<OrganizationAvatar organization={organization} />
@@ -57,20 +98,16 @@ export default function OrganizationNavigationHeader({ organization, organizatio
) : (
<span className="spacer-left">{organization.name}</span>
)}
- {organization.alm && (
- <a
- className="link-no-underline"
- href={organization.alm.url}
- rel="noopener noreferrer"
- target="_blank">
+ {almKey && (
+ <Tooltip mouseLeaveDelay={0.25} overlay={tooltipContent}>
<img
- alt={sanitizeAlmId(organization.alm.key)}
- className="text-text-top spacer-left"
+ alt={translate(almKey)}
+ className="text-middle spacer-left"
height={16}
- src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.alm.key)}.svg`}
+ src={tooltipIconSrc}
width={16}
/>
- </a>
+ </Tooltip>
)}
{organization.description != null && (
<div className="navbar-context-description">
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeaderContainer.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeaderContainer.tsx
deleted file mode 100644
index 999b68b635f..00000000000
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeaderContainer.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { connect } from 'react-redux';
-import OrganizationNavigationHeader from './OrganizationNavigationHeader';
-import { getMyOrganizations, Store } from '../../../store/rootReducer';
-
-interface StateProps {
- organizations: T.Organization[];
-}
-
-const mapStateToProps = (state: Store): StateProps => ({
- organizations: getMyOrganizations(state)
-});
-
-export default connect(mapStateToProps)(OrganizationNavigationHeader);
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
index 39519ffd939..12f1d27fb4c 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
@@ -19,28 +19,22 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import OrganizationNavigationHeader from '../OrganizationNavigationHeader';
-import { mockOrganizationWithAlm } from '../../../../helpers/testMocks';
+import OrganizationNavigationHeader, { Props } from '../OrganizationNavigationHeader';
+import { mockOrganizationWithAlm, mockCurrentUser } from '../../../../helpers/testMocks';
it('renders', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('renders with alm integration', () => {
expect(
- shallow(
- <OrganizationNavigationHeader
- organization={{ key: 'foo', name: 'Foo', projectVisibility: 'public' }}
- organizations={[]}
- />
- )
+ shallowRender({ organization: mockOrganizationWithAlm({ projectVisibility: 'public' }) })
).toMatchSnapshot();
});
-it('renders with alm integration', () => {
+it('renders for external user w/o alm integration', () => {
expect(
- shallow(
- <OrganizationNavigationHeader
- organization={mockOrganizationWithAlm({ projectVisibility: 'public' })}
- organizations={[]}
- />
- )
+ shallowRender({ currentUser: mockCurrentUser({ externalProvider: 'github' }) })
).toMatchSnapshot();
});
@@ -49,15 +43,23 @@ it('renders dropdown', () => {
{ actions: { admin: true }, key: 'org1', name: 'org1', projectVisibility: 'public' },
{ actions: { admin: false }, key: 'org2', name: 'org2', projectVisibility: 'public' }
];
- const wrapper = shallow(
+ const wrapper = shallowRender({
+ organizations
+ });
+ expect(wrapper.find('Dropdown')).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
<OrganizationNavigationHeader
+ currentUser={mockCurrentUser()}
organization={{
key: 'foo',
name: 'Foo',
projectVisibility: 'public'
}}
- organizations={organizations}
+ organizations={[]}
+ {...props}
/>
);
- expect(wrapper.find('Dropdown')).toMatchSnapshot();
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
index 0da0558483a..1db61a2ee48 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
@@ -8,7 +8,12 @@ exports[`render 1`] = `
<div
className="navbar-context-justified"
>
- <Connect(OrganizationNavigationHeader)
+ <OrganizationNavigationHeader
+ currentUser={
+ Object {
+ "isLoggedIn": false,
+ }
+ }
organization={
Object {
"key": "foo",
@@ -16,6 +21,7 @@ exports[`render 1`] = `
"projectVisibility": "public",
}
}
+ organizations={Array []}
/>
<OrganizationNavigationMeta
currentUser={
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
index 27d6f917f78..da8dbbc2c87 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
@@ -11,6 +11,16 @@ exports[`renders 1`] = `
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
+ to="/organizations/foo/edit"
+ >
+ organization.settings
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
to="/organizations/foo/groups"
>
user_groups.page
@@ -56,16 +66,6 @@ exports[`renders 1`] = `
webhooks.page
</Link>
</li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/edit"
- >
- edit
- </Link>
- </li>
</ul>
}
tagName="li"
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
index c02c0831a7f..c580daad4e2 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
@@ -67,6 +67,55 @@ exports[`renders dropdown 1`] = `
</Dropdown>
`;
+exports[`renders for external user w/o alm integration 1`] = `
+<header
+ className="navbar-context-header"
+>
+ <OrganizationAvatar
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <span
+ className="spacer-left"
+ >
+ Foo
+ </span>
+ <Tooltip
+ mouseLeaveDelay={0.25}
+ overlay={
+ <React.Fragment>
+ <p>
+ organization.not_bound_to_x.github
+ </p>
+ <hr
+ className="spacer-top spacer-bottom"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/edit"
+ >
+ organization.go_to_settings_to_bind
+ </Link>
+ </React.Fragment>
+ }
+ >
+ <img
+ alt="github"
+ className="text-middle spacer-left"
+ height={16}
+ src="/images/sonarcloud/github-unbound.svg"
+ width={16}
+ />
+ </Tooltip>
+</header>
+`;
+
exports[`renders with alm integration 1`] = `
<header
className="navbar-context-header"
@@ -90,19 +139,33 @@ exports[`renders with alm integration 1`] = `
>
Foo
</span>
- <a
- className="link-no-underline"
- href="https://github.com/foo"
- rel="noopener noreferrer"
- target="_blank"
+ <Tooltip
+ mouseLeaveDelay={0.25}
+ overlay={
+ <React.Fragment>
+ <p>
+ organization.bound_to_x.github
+ </p>
+ <hr
+ className="spacer-top spacer-bottom"
+ />
+ <a
+ href="https://github.com/foo"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ organization.see_on_x.github
+ </a>
+ </React.Fragment>
+ }
>
<img
alt="github"
- className="text-text-top spacer-left"
+ className="text-middle spacer-left"
height={16}
src="/images/sonarcloud/github.svg"
width={16}
/>
- </a>
+ </Tooltip>
</header>
`;
diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
index 871963945d7..fa4a52d5b96 100644
--- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
+++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
@@ -93,9 +93,8 @@
.navbar-context-description {
display: inline-block;
line-height: var(--controlHeight);
- margin-left: 16px;
+ margin-left: var(--gridSize);
padding-top: 4px;
- padding-left: 4px;
color: var(--secondFontColor);
font-size: var(--smallFontSize);
}
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
index b2a09af3ab0..de98abb4968 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
@@ -23,8 +23,10 @@ import {
isPersonal,
isVSTS,
sanitizeAlmId,
- getAlmMembersUrl
+ getAlmMembersUrl,
+ getUserAlmKey
} from '../almIntegrations';
+import { mockCurrentUser } from '../testMocks';
it('#getAlmMembersUrl', () => {
expect(getAlmMembersUrl('github', 'https://github.com/Foo')).toBe(
@@ -69,3 +71,16 @@ it('#sanitizeAlmId', () => {
expect(sanitizeAlmId('bitbucket')).toBe('bitbucket');
expect(sanitizeAlmId('github')).toBe('github');
});
+
+describe('getUserAlmKey', () => {
+ it('should return sanitized almKey', () => {
+ expect(getUserAlmKey(mockCurrentUser({ externalProvider: 'bitbucketcloud' }))).toBe(
+ 'bitbucket'
+ );
+ });
+
+ it('should return undefined', () => {
+ expect(getUserAlmKey(mockCurrentUser())).toBeUndefined();
+ expect(getUserAlmKey(mockCurrentUser({ isLoggedIn: undefined }))).toBeUndefined();
+ });
+});
diff --git a/server/sonar-web/src/main/js/helpers/almIntegrations.ts b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
index c3cfc0744a3..dfe28e3dd53 100644
--- a/server/sonar-web/src/main/js/helpers/almIntegrations.ts
+++ b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
@@ -29,6 +29,12 @@ export function getAlmMembersUrl(key: string, url: string): string {
return url + 'profile/members';
}
+export function getUserAlmKey(user: T.CurrentUser) {
+ return isLoggedIn(user) && user.externalProvider
+ ? sanitizeAlmId(user.externalProvider)
+ : undefined;
+}
+
export function hasAdvancedALMIntegration(user: T.CurrentUser) {
return (
isLoggedIn(user) && (isBitbucket(user.externalProvider) || isGithub(user.externalProvider))
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 17ffc9ce130..9ba97fab7d9 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2647,6 +2647,11 @@ about_page.scanners.ant=SonarQube Scanner for Ant
organization.avatar=Avatar
organization.avatar.description=Url of a small image that represents the organization (preferably 30px height).
organization.avatar.preview=Preview
+organization.bind_to_x=Bind this organization to {0}
+organization.go_to_settings_to_bind=Go to Organization Settings to bind it.
+organization.bound=This organization is bound.
+organization.bound_to_x=This organization is bound to {0}
+organization.not_bound_to_x=This organization is not bound to {0}
organization.created=Organization "{0}" has been created.
organization.delete=Delete Organization
organization.delete_x=Delete the "{0}" organization
@@ -2657,14 +2662,18 @@ organization.deleted=Organization has been deleted.
organization.deleted_x=Organization "{0}" has been deleted.
organization.description=Description
organization.description.description=Description of the organization.
-organization.edit=Edit Organization
+organization.details=Organization details
organization.key=Key
organization.key.description=Key of the organization (up to 255 characters). All chars must be lower-case letters (a to z), digits or dash (but dash can neither be trailing nor heading). When not specified, the key is computed from the name.
organization.name=Name
organization.name.description=Name of the organization (up to 255 characters).
+organization.see_on_x=See on {0}
+organization.settings=Organization settings
organization.updated=Organization details have been updated.
organization.url=Url
organization.url.description=Url of the homepage of the organization.
+organization.binding_with_x_easy_sync=Binding an organization from SonarCloud with {0} is an easy way to keep them synchronized.
+organization.app_will_be_installed_on_x=To bind this organization to {0}, the SonarCloud application will be installed.
organization.members.page=Members
organization.members.page.description=Add users to the organization and grant them permissions to work on the projects. See {link} documentation.
organization.members.add=Add a member
@@ -2695,7 +2704,7 @@ organization.paid_plan.badge=Paid plan
organization.default_visibility_of_new_projects=Default visibility of new projects:
organization.change_visibility_form.header=Set Default Visibility of New Projects
organization.change_visibility_form.warning=This will not change the visibility of already existing projects.organization.change_visibility_form.submit=Change Default Visibility
-
+organization.bind.success=Organization bound successfully
#------------------------------------------------------------------------------
#
@@ -2827,6 +2836,8 @@ onboarding.import_organization.org_not_found.tips_2=Try to uninstall and re-inst
onboarding.import_organization.choose_organization=Choose an organization...
onboarding.import_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket
onboarding.import_organization.choose_organization_button.github=Choose an organization on GitHub
+onboarding.import_organization.choose_the_organization_button.bitbucket=Choose the team on Bitbucket
+onboarding.import_organization.choose_the_organization_button.github=Choose the organization on GitHub
onboarding.import_organization.installing=Finalize installation of the {0} application...
onboarding.import_organization.personal.page.header=Bind to your personal organization
onboarding.import_organization.personal.import_org_details=Import personal organization details
@@ -2839,7 +2850,7 @@ onboarding.import_organization.members_sync_info_x=All members from your {0} {1}
onboarding.import_organization.bind_members_not_sync_info_x=We'll keep your members, groups and permissions as they are today on SonarCloud. To sync your members with your {0}, enable members sync in your Members tab.
onboarding.import_organization_x=Import {avatar} {name} into a SonarCloud organization
onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName}
-
+onboarding.binding_organization=Binding organization
onboarding.team.header=Join a team
onboarding.team.first_step=Well congrats, the first step is done!