aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2018-10-15 11:55:35 +0200
committerSonarTech <sonartech@sonarsource.com>2018-11-16 20:21:03 +0100
commitf61d654f7d74036f50f2a1ca6b380a437ec911a2 (patch)
tree9a89718368daa1b60d5547ef606fbd00ebca59fc /server
parentf8694e7d8b50651cba439fb5847ffde2d3bd013c (diff)
downloadsonarqube-f61d654f7d74036f50f2a1ca6b380a437ec911a2.tar.gz
sonarqube-f61d654f7d74036f50f2a1ca6b380a437ec911a2.zip
SONAR-11321 Create organization from GitHub organization or BitBucket team
* Create api/alm_integration/show_organization and handle only GitHub installation * Add import from ALM tab in Create Org page * Do not show error while validating detail input * Add step to create organization from ALM * Display a warning if the installation id was not found * Add Alm link to remote organization in org context * Create GET api/alm_integration/show_app_info
Diffstat (limited to 'server')
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallDao.java5
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallMapper.java3
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/alm/AlmAppInstallMapper.xml10
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/alm/AlmAppInstallDaoTest.java13
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java20
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java20
-rw-r--r--server/sonar-web/src/main/js/api/alm-integration.ts16
-rw-r--r--server/sonar-web/src/main/js/api/organizations.ts6
-rw-r--r--server/sonar-web/src/main/js/app/components/Landing.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/components/StartupModal.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx9
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/menu.css2
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/forms.css6
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/icons.css4
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css5
-rw-r--r--server/sonar-web/src/main/js/app/styles/sonarcloud.css36
-rw-r--r--server/sonar-web/src/main/js/app/types.ts14
-rw-r--r--server/sonar-web/src/main/js/apps/about/sonarcloud/AsAService.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/about/sonarcloud/AzureDevOps.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/about/sonarcloud/BranchAnalysis.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/about/sonarcloud/Contact.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/about/sonarcloud/SQHome.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/about/sonarcloud/SonarLintIntegration.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx104
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx222
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx133
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx74
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx122
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx86
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap58
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap60
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap241
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap74
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap5
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/utils-test.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/utils.ts16
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap37
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/SonarCloudEmptyOverview.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap116
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/controls/Tabs.css60
-rw-r--r--server/sonar-web/src/main/js/components/controls/Tabs.tsx77
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/Tabs-test.tsx75
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tabs-test.tsx.snap52
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.tsx3
-rw-r--r--server/sonar-web/src/main/js/helpers/almIntegrations.ts8
-rw-r--r--server/sonar-web/src/main/js/helpers/organizations.ts3
-rw-r--r--server/sonar-web/src/main/js/helpers/users.ts31
-rw-r--r--server/sonar-web/src/main/js/store/users.ts14
77 files changed, 1881 insertions, 417 deletions
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallDao.java
index 03361e5b586..04302fe6f06 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallDao.java
@@ -52,6 +52,11 @@ public class AlmAppInstallDao implements Dao {
return Optional.ofNullable(mapper.selectByOwner(alm.getId(), ownerId));
}
+ public Optional<String> getOwerId(DbSession dbSession, ALM alm, String installationId) {
+ AlmAppInstallMapper mapper = getMapper(dbSession);
+ return Optional.ofNullable(mapper.selectOwnerId(alm.getId(), installationId));
+ }
+
public List<AlmAppInstallDto> findAllWithNoOwnerType(DbSession dbSession) {
return getMapper(dbSession).selectAllWithNoOwnerType();
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallMapper.java
index 809a054ca26..cb979c2217f 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallMapper.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/AlmAppInstallMapper.java
@@ -29,6 +29,9 @@ public interface AlmAppInstallMapper {
@CheckForNull
AlmAppInstallDto selectByOwner(@Param("almId") String almId, @Param("ownerId") String ownerId);
+ @CheckForNull
+ String selectOwnerId(@Param("almId") String almId, @Param("installId") String installId);
+
List<AlmAppInstallDto> selectAllWithNoOwnerType();
void insert(@Param("uuid") String uuid, @Param("almId") String almId, @Param("ownerId") String ownerId,
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/AlmAppInstallMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/AlmAppInstallMapper.xml
index 2454efde08c..d96a33dfe13 100644
--- a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/AlmAppInstallMapper.xml
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/AlmAppInstallMapper.xml
@@ -22,6 +22,16 @@
and owner_id = #{ownerId, jdbcType=VARCHAR}
</select>
+ <select id="selectOwnerId" parameterType="Map" resultType="String">
+ select
+ owner_id as ownerId
+ from
+ alm_app_installs
+ where
+ alm_id = #{almId, jdbcType=VARCHAR}
+ and install_id = #{installId, jdbcType=VARCHAR}
+ </select>
+
<select id="selectAllWithNoOwnerType" parameterType="Map" resultType="org.sonar.db.alm.AlmAppInstallDto">
select <include refid="sqlColumns" />
from
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/alm/AlmAppInstallDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/alm/AlmAppInstallDaoTest.java
index 5a15a5a04ab..00955915f03 100644
--- a/server/sonar-db-dao/src/test/java/org/sonar/db/alm/AlmAppInstallDaoTest.java
+++ b/server/sonar-db-dao/src/test/java/org/sonar/db/alm/AlmAppInstallDaoTest.java
@@ -72,7 +72,6 @@ public class AlmAppInstallDaoTest {
assertThat(underTest.selectByOwner(dbSession, BITBUCKETCLOUD, A_OWNER)).isEmpty();
}
-
@Test
public void selectByOwner_throws_NPE_when_alm_is_null() {
expectAlmNPE();
@@ -95,6 +94,16 @@ public class AlmAppInstallDaoTest {
}
@Test
+ public void getOwnerId() {
+ when(uuidFactory.create()).thenReturn(A_UUID);
+ underTest.insertOrUpdate(dbSession, GITHUB, A_OWNER, true, AN_INSTALL);
+
+ assertThat(underTest.getOwerId(dbSession, GITHUB, AN_INSTALL)).contains(A_OWNER);
+ assertThat(underTest.getOwerId(dbSession, GITHUB, "unknown")).isEmpty();
+ assertThat(underTest.getOwerId(dbSession, BITBUCKETCLOUD, AN_INSTALL)).isEmpty();
+ }
+
+ @Test
public void insert_throws_NPE_if_alm_is_null() {
expectAlmNPE();
@@ -170,7 +179,7 @@ public class AlmAppInstallDaoTest {
underTest.insertOrUpdate(dbSession, GITHUB, A_OWNER, true, AN_INSTALL);
when(system2.now()).thenReturn(DATE_LATER);
- underTest.insertOrUpdate(dbSession, GITHUB, A_OWNER, true, OTHER_INSTALL);
+ underTest.insertOrUpdate(dbSession, GITHUB, A_OWNER,true, OTHER_INSTALL);
assertThatAlmAppInstall(GITHUB, A_OWNER)
.hasInstallId(OTHER_INSTALL)
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java
index 190bdecd3ed..8a51b94e839 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java
@@ -38,6 +38,7 @@ import org.sonar.server.exceptions.UnauthorizedException;
import static java.lang.String.format;
import static org.apache.commons.lang.StringUtils.defaultString;
+import static org.sonar.server.user.UserSession.IdentityProvider.SONARQUBE;
public abstract class AbstractUserSession implements UserSession {
private static final Set<String> PUBLIC_PERMISSIONS = ImmutableSet.of(UserRole.USER, UserRole.CODEVIEWER);
@@ -45,26 +46,15 @@ public abstract class AbstractUserSession implements UserSession {
private static final String AUTHENTICATION_IS_REQUIRED_MESSAGE = "Authentication is required";
protected static Identity computeIdentity(UserDto userDto) {
- switch (userDto.getExternalIdentityProvider()) {
- case "github":
- return new Identity(IdentityProvider.GITHUB, externalIdentityOf(userDto));
- case "bitbucket":
- return new Identity(IdentityProvider.BITBUCKET, externalIdentityOf(userDto));
- case "sonarqube":
- return new Identity(IdentityProvider.SONARQUBE, null);
- default:
- return new Identity(IdentityProvider.OTHER, externalIdentityOf(userDto));
- }
+ IdentityProvider identityProvider = IdentityProvider.getFromKey(userDto.getExternalIdentityProvider());
+ ExternalIdentity externalIdentity = identityProvider == SONARQUBE ? null : externalIdentityOf(userDto);
+ return new Identity(identityProvider, externalIdentity);
}
- @CheckForNull
private static ExternalIdentity externalIdentityOf(UserDto userDto) {
String externalId = userDto.getExternalId();
String externalLogin = userDto.getExternalLogin();
- if (externalId == null && externalLogin == null) {
- return null;
- }
- return new ExternalIdentity(externalId == null ? externalLogin : externalId, externalLogin);
+ return new ExternalIdentity(externalId, externalLogin);
}
protected static final class Identity {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java
index 7252ea912e1..9f898109d57 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java
@@ -19,6 +19,7 @@
*/
package org.sonar.server.user;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -72,7 +73,24 @@ public interface UserSession {
* This enum supports by name only the few providers for which specific code exists.
*/
enum IdentityProvider {
- SONARQUBE, GITHUB, BITBUCKET, OTHER
+ SONARQUBE("sonarqube"), GITHUB("github"), BITBUCKET("bitbucket"), OTHER("other");
+
+ String key;
+
+ IdentityProvider(String key) {
+ this.key = key;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public static IdentityProvider getFromKey(String key) {
+ return Arrays.stream(IdentityProvider.values())
+ .filter(i -> i.getKey().equals(key))
+ .findAny()
+ .orElse(OTHER);
+ }
}
/**
diff --git a/server/sonar-web/src/main/js/api/alm-integration.ts b/server/sonar-web/src/main/js/api/alm-integration.ts
index d35ff9995e0..c3f560044f2 100644
--- a/server/sonar-web/src/main/js/api/alm-integration.ts
+++ b/server/sonar-web/src/main/js/api/alm-integration.ts
@@ -18,9 +18,23 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { getJSON, postJSON } from '../helpers/request';
-import { AlmRepository } from '../app/types';
+import { AlmRepository, AlmApplication, AlmOrganization } from '../app/types';
import throwGlobalError from '../app/utils/throwGlobalError';
+export function getAlmAppInfo(): Promise<{ application: AlmApplication }> {
+ return getJSON('/api/alm_integration/show_app_info').catch(throwGlobalError);
+}
+
+export function getAlmOrganization(data: { installationId: string }): Promise<AlmOrganization> {
+ return getJSON('/api/alm_integration/show_organization', data).then(
+ ({ organization }) => ({
+ ...organization,
+ name: organization.name || organization.key
+ }),
+ throwGlobalError
+ );
+}
+
export function getRepositories(): Promise<{
almIntegration: {
installed: boolean;
diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts
index 289d6ce99d5..1b72037ce83 100644
--- a/server/sonar-web/src/main/js/api/organizations.ts
+++ b/server/sonar-web/src/main/js/api/organizations.ts
@@ -39,12 +39,12 @@ export function getOrganization(key: string): Promise<Organization | undefined>
}
interface GetOrganizationNavigation {
+ adminPages: Array<{ key: string; name: string }>;
canAdmin: boolean;
canDelete: boolean;
canProvisionProjects: boolean;
isDefault: boolean;
pages: Array<{ key: string; name: string }>;
- adminPages: Array<{ key: string; name: string }>;
}
export function getOrganizationNavigation(key: string): Promise<GetOrganizationNavigation> {
@@ -54,7 +54,9 @@ export function getOrganizationNavigation(key: string): Promise<GetOrganizationN
);
}
-export function createOrganization(data: OrganizationBase): Promise<Organization> {
+export function createOrganization(
+ data: OrganizationBase & { installId?: string }
+): Promise<Organization> {
return postJSON('/api/organizations/create', data).then(r => r.organization, throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/app/components/Landing.tsx b/server/sonar-web/src/main/js/app/components/Landing.tsx
index f96be5fc011..421c6cceb7c 100644
--- a/server/sonar-web/src/main/js/app/components/Landing.tsx
+++ b/server/sonar-web/src/main/js/app/components/Landing.tsx
@@ -21,9 +21,10 @@ import * as React from 'react';
import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Location } from 'history';
-import { CurrentUser, isLoggedIn } from '../types';
+import { CurrentUser } from '../types';
import { getCurrentUser, Store } from '../../store/rootReducer';
import { getHomePageUrl } from '../../helpers/urls';
+import { isLoggedIn } from '../../helpers/users';
interface StateProps {
currentUser: CurrentUser | undefined;
diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
index a5f0263139a..c0393198c15 100644
--- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx
+++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
@@ -21,7 +21,7 @@ import * as React from 'react';
import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter, WithRouterProps } from 'react-router';
-import { CurrentUser, isLoggedIn } from '../types';
+import { CurrentUser } from '../types';
import { differenceInDays, parseDate, toShortNotSoISOString } from '../../helpers/dates';
import { EditionKey } from '../../apps/marketplace/utils';
import { getCurrentUser, getAppState, Store } from '../../store/rootReducer';
@@ -32,6 +32,7 @@ import { save, get } from '../../helpers/storage';
import { isSonarCloud } from '../../helpers/system';
import { skipOnboarding } from '../../api/users';
import { lazyLoad } from '../../components/lazyLoad';
+import { isLoggedIn } from '../../helpers/users';
const OnboardingModal = lazyLoad(() => import('../../apps/tutorials/onboarding/OnboardingModal'));
const LicensePromptModal = lazyLoad(
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
index d6f38f12a29..02d69091357 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
@@ -24,7 +24,6 @@ import {
BranchLike,
Component,
CurrentUser,
- isLoggedIn,
HomePageType,
HomePage,
Measure
@@ -43,6 +42,7 @@ import {
isPullRequest
} from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
+import { isLoggedIn } from '../../../../helpers/users';
import { getCurrentUser, Store } from '../../../../store/rootReducer';
interface StateProps {
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 0eda00d3b95..67cda337eae 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
@@ -27,12 +27,13 @@ import GlobalNavUserContainer from './GlobalNavUserContainer';
import Search from '../../search/Search';
import EmbedDocsPopupHelper from '../../embed-docs-modal/EmbedDocsPopupHelper';
import * as theme from '../../../theme';
-import { CurrentUser, AppState, isLoggedIn } from '../../../types';
+import { CurrentUser, AppState } from '../../../types';
import NavBar from '../../../../components/nav/NavBar';
import { lazyLoad } from '../../../../components/lazyLoad';
import { getCurrentUser, getAppState, Store } from '../../../../store/rootReducer';
import { SuggestionLink } from '../../embed-docs-modal/SuggestionsProvider';
import { isSonarCloud } from '../../../../helpers/system';
+import { isLoggedIn } from '../../../../helpers/users';
import './GlobalNav.css';
const GlobalNavPlus = lazyLoad(() => import('./GlobalNavPlus'), 'GlobalNavPlus');
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
index 8856840d3f1..6ff59a0854d 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
@@ -20,13 +20,14 @@
import * as React from 'react';
import * as classNames from 'classnames';
import { Link } from 'react-router';
-import { isLoggedIn, CurrentUser, AppState, Extension } from '../../../types';
+import { CurrentUser, AppState, Extension } from '../../../types';
import { translate } from '../../../../helpers/l10n';
import { getQualityGatesUrl, getBaseUrl } from '../../../../helpers/urls';
import { isMySet } from '../../../../apps/issues/utils';
import Dropdown from '../../../../components/controls/Dropdown';
import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
import { isSonarCloud } from '../../../../helpers/system';
+import { isLoggedIn } from '../../../../helpers/users';
interface Props {
appState: Pick<AppState, 'canAdmin' | 'globalPages' | 'organizationsEnabled' | 'qualifiers'>;
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 99d30116560..4e3f45d648b 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
@@ -22,12 +22,13 @@ 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, LoggedInUser } from '../../../types';
-import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../helpers/urls';
+import { AppState, LoggedInUser } from '../../../types';
import { getExtensionStart } from '../../extensions/utils';
-import { isSonarCloud } from '../../../../helpers/system';
-import { translate } from '../../../../helpers/l10n';
import { getComponentNavigation } from '../../../../api/nav';
+import { translate } from '../../../../helpers/l10n';
+import { isSonarCloud } from '../../../../helpers/system';
+import { getPortfolioAdminUrl, getPortfolioUrl } from '../../../../helpers/urls';
+import { hasGlobalPermission } from '../../../../helpers/users';
interface Props {
appState: Pick<AppState, 'qualifiers'>;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
index 8d11216b9f7..530f17d248f 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
@@ -22,12 +22,13 @@ import { sortBy } from 'lodash';
import * as PropTypes from 'prop-types';
import { Link } from 'react-router';
import * as theme from '../../../theme';
-import { CurrentUser, LoggedInUser, isLoggedIn, Organization } from '../../../types';
+import { CurrentUser, LoggedInUser, Organization } from '../../../types';
import Avatar from '../../../../components/ui/Avatar';
import OrganizationListItem from '../../../../components/ui/OrganizationListItem';
import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/urls';
import Dropdown from '../../../../components/controls/Dropdown';
+import { isLoggedIn } from '../../../../helpers/users';
interface Props {
appState: { organizationsEnabled?: boolean };
diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css
index 9bead9c66d3..b11b91e4591 100644
--- a/server/sonar-web/src/main/js/app/styles/components/menu.css
+++ b/server/sonar-web/src/main/js/app/styles/components/menu.css
@@ -72,7 +72,7 @@
}
.menu > li > a.disabled {
- color: #bbb !important;
+ color: var(--disableGrayText) !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
diff --git a/server/sonar-web/src/main/js/app/styles/init/forms.css b/server/sonar-web/src/main/js/app/styles/init/forms.css
index d68a4eb42d8..48af0f4a458 100644
--- a/server/sonar-web/src/main/js/app/styles/init/forms.css
+++ b/server/sonar-web/src/main/js/app/styles/init/forms.css
@@ -249,8 +249,8 @@ label[for] {
}
.radio-toggle input[type='radio']:disabled + label {
- color: #bbb;
- border-color: #ddd;
- background: #ebebeb;
+ color: var(--disableGrayText);
+ border-color: var(--disableGrayBorder);
+ background: var(--disableGrayBg);
cursor: not-allowed;
}
diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css
index 8acdf0079e1..3c96cec1bdc 100644
--- a/server/sonar-web/src/main/js/app/styles/init/icons.css
+++ b/server/sonar-web/src/main/js/app/styles/init/icons.css
@@ -84,12 +84,12 @@ a[class*=' icon-'] {
}
.icon-checkbox-disabled:before {
- border: 1px solid #bbb;
+ border: 1px solid var(--disableGrayText);
cursor: not-allowed;
}
.icon-checkbox-disabled.icon-checkbox-checked:before {
- background-color: #bbb;
+ background-color: var(--disableGrayText);
}
.icon-checkbox-invisible {
diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css
index 8498249f48d..9cdc8dafa40 100644
--- a/server/sonar-web/src/main/js/app/styles/init/misc.css
+++ b/server/sonar-web/src/main/js/app/styles/init/misc.css
@@ -299,6 +299,11 @@ td.big-spacer-top {
align-items: center;
}
+.display-inline-flex-baseline {
+ display: inline-flex !important;
+ align-items: baseline;
+}
+
.display-inline-flex-center {
display: inline-flex !important;
align-items: center;
diff --git a/server/sonar-web/src/main/js/app/styles/sonarcloud.css b/server/sonar-web/src/main/js/app/styles/sonarcloud.css
index 4be34dc3c7a..748afde887a 100644
--- a/server/sonar-web/src/main/js/app/styles/sonarcloud.css
+++ b/server/sonar-web/src/main/js/app/styles/sonarcloud.css
@@ -33,42 +33,6 @@
font-weight: bold;
}
-.sonarcloud .flex-tabs {
- display: flex;
- clear: left;
- margin-bottom: calc(3 * var(--gridSize));
- border-bottom: 1px solid var(--barBorderColor);
- font-size: var(--mediumFontSize);
-}
-
-.sonarcloud .flex-tabs > li > a {
- position: relative;
- display: block;
- top: 1px;
- height: 100%;
- width: 100%;
- box-sizing: border-box;
- color: var(--secondFontColor);
- font-weight: 600;
- cursor: pointer;
- padding-bottom: calc(1.5 * var(--gridSize));
- border-bottom: 3px solid transparent;
- transition: color 0.2s ease;
-}
-
-.sonarcloud .flex-tabs > li ~ li {
- margin-left: calc(4 * var(--gridSize));
-}
-
-.sonarcloud .flex-tabs > li > a:hover {
- color: var(--baseFontColor);
-}
-
-.sonarcloud .flex-tabs > li > a.selected {
- color: var(--blue);
- border-bottom-color: var(--blue);
-}
-
.beta-badge {
display: inline-block;
padding: 2px 4px;
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index 8e36aa98591..803ba673ad2 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -23,6 +23,14 @@ export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Type ordered alphabetically to prevent merge conflicts
+export interface AlmApplication extends IdentityProvider {
+ installationUrl: string;
+}
+export interface AlmOrganization extends OrganizationBase {
+ key: string;
+ type: 'ORGANIZATION' | 'USER';
+}
+
export interface AlmRepository {
label: string;
installationKey: string;
@@ -277,10 +285,6 @@ export interface IdentityProvider {
name: string;
}
-export function isLoggedIn(user: CurrentUser): user is LoggedInUser {
- return user.isLoggedIn;
-}
-
export function hasGlobalPermission(user: CurrentUser, permission: string): boolean {
if (!user.permissions) {
return false;
@@ -476,6 +480,8 @@ export interface Notification {
}
export interface Organization extends OrganizationBase {
+ almId?: string;
+ almRepoUrl?: string;
adminPages?: Extension[];
canAdmin?: boolean;
canDelete?: boolean;
diff --git a/server/sonar-web/src/main/js/apps/about/sonarcloud/AsAService.tsx b/server/sonar-web/src/main/js/apps/about/sonarcloud/AsAService.tsx
index c113f0f46e0..0c387509bc5 100644
--- a/server/sonar-web/src/main/js/apps/about/sonarcloud/AsAService.tsx
+++ b/server/sonar-web/src/main/js/apps/about/sonarcloud/AsAService.tsx
@@ -22,7 +22,7 @@ import Helmet from 'react-helmet';
import SQPageContainer from './components/SQPageContainer';
import SQStartUsing from './components/SQStartUsing';
import SQTopNav from './components/SQTopNav';
-import { isLoggedIn } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
import { getBaseUrl } from '../../../helpers/urls';
import './style.css';
diff --git a/server/sonar-web/src/main/js/apps/about/sonarcloud/AzureDevOps.tsx b/server/sonar-web/src/main/js/apps/about/sonarcloud/AzureDevOps.tsx
index 783eba0868d..da9bea478da 100644
--- a/server/sonar-web/src/main/js/apps/about/sonarcloud/AzureDevOps.tsx
+++ b/server/sonar-web/src/main/js/apps/about/sonarcloud/AzureDevOps.tsx
@@ -21,7 +21,7 @@ import * as React from 'react';
import Helmet from 'react-helmet';
import { Link } from 'react-router';
import SQPageContainer from './components/SQPageContainer';
-import { isLoggedIn } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
import { getBaseUrl } from '../../../helpers/urls';
import './style.css';
diff --git a/server/sonar-web/src/main/js/apps/about/sonarcloud/BranchAnalysis.tsx b/server/sonar-web/src/main/js/apps/about/sonarcloud/BranchAnalysis.tsx
index 54e8e2214cf..d5bcb2932a8 100644
--- a/server/sonar-web/src/main/js/apps/about/sonarcloud/BranchAnalysis.tsx
+++ b/server/sonar-web/src/main/js/apps/about/sonarcloud/BranchAnalysis.tsx
@@ -22,7 +22,7 @@ import Helmet from 'react-helmet';
import SQPageContainer from './components/SQPageContainer';
import SQStartUsing from './components/SQStartUsing';
import SQTopNav from './components/SQTopNav';
-import { isLoggedIn } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
import { getBaseUrl } from '../../../helpers/urls';
import './style.css';
diff --git a/server/sonar-web/src/main/js/apps/about/sonarcloud/Contact.tsx b/server/sonar-web/src/main/js/apps/about/sonarcloud/Contact.tsx
index 65c70964eb8..9153e103b18 100644
--- a/server/sonar-web/src/main/js/apps/about/sonarcloud/Contact.tsx
+++ b/server/sonar-web/src/main/js/apps/about/sonarcloud/Contact.tsx
@@ -23,8 +23,9 @@ import { Link } from 'react-router';
import { Location } from 'history';
import SQPageContainer from './components/SQPageContainer';
import Select from '../../../components/controls/Select';
-import { isLoggedIn, Organization } from '../../../app/types';
import { Alert } from '../../../components/ui/Alert';
+import { Organization } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
import './style.css';
const CATEGORIES = [
diff --git a/server/sonar-web/src/main/js/apps/about/sonarcloud/SQHome.tsx b/server/sonar-web/src/main/js/apps/about/sonarcloud/SQHome.tsx
index 3f085d60410..f826c42cf8a 100644
--- a/server/sonar-web/src/main/js/apps/about/sonarcloud/SQHome.tsx
+++ b/server/sonar-web/src/main/js/apps/about/sonarcloud/SQHome.tsx
@@ -24,7 +24,7 @@ import LoginButtons from './components/LoginButtons';
import Pricing from './components/Pricing';
import SQPageContainer from './components/SQPageContainer';
import StartUsing from './components/StartUsing';
-import { isLoggedIn } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
import { getBaseUrl } from '../../../helpers/urls';
import './style.css';
diff --git a/server/sonar-web/src/main/js/apps/about/sonarcloud/SonarLintIntegration.tsx b/server/sonar-web/src/main/js/apps/about/sonarcloud/SonarLintIntegration.tsx
index 98510d622fa..a9e35acb217 100644
--- a/server/sonar-web/src/main/js/apps/about/sonarcloud/SonarLintIntegration.tsx
+++ b/server/sonar-web/src/main/js/apps/about/sonarcloud/SonarLintIntegration.tsx
@@ -22,7 +22,7 @@ import Helmet from 'react-helmet';
import SQPageContainer from './components/SQPageContainer';
import SQStartUsing from './components/SQStartUsing';
import SQTopNav from './components/SQTopNav';
-import { isLoggedIn } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
import { getBaseUrl } from '../../../helpers/urls';
import './style.css';
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
index 7eee54217b6..7d795c8d1f8 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
@@ -41,13 +41,13 @@ import {
ComponentMeasure,
ComponentMeasureEnhanced,
CurrentUser,
- isLoggedIn,
Metric,
Paging,
MeasureEnhanced,
Period
} from '../../../app/types';
import { RequestData } from '../../../helpers/request';
+import { isLoggedIn } from '../../../helpers/users';
interface Props {
branchLike?: BranchLike;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
new file mode 100644
index 00000000000..f7882de2ba9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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';
+import { FormattedMessage } from 'react-intl';
+import ChooseRemoteOrganizationStep from './ChooseRemoteOrganizationStep';
+import OrganizationDetailsStep from './OrganizationDetailsStep';
+import {
+ AlmApplication,
+ AlmOrganization,
+ OrganizationBase,
+ Organization
+} from '../../../app/types';
+import { getBaseUrl } from '../../../helpers/urls';
+import { translate } from '../../../helpers/l10n';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+
+interface Props {
+ almApplication: AlmApplication;
+ almInstallId?: string;
+ almOrganization?: AlmOrganization;
+ createOrganization: (
+ organization: OrganizationBase & { installId?: string }
+ ) => Promise<Organization>;
+ onOrgCreated: (organization: string) => void;
+}
+
+export default class AutoOrganizationCreate extends React.PureComponent<Props> {
+ handleCreateOrganization = (organization: Required<OrganizationBase>) => {
+ if (organization) {
+ return this.props
+ .createOrganization({
+ avatar: organization.avatar,
+ description: organization.description,
+ installId: this.props.almInstallId,
+ key: organization.key,
+ name: organization.name || organization.key,
+ url: organization.url
+ })
+ .then(({ key }) => this.props.onOrgCreated(key));
+ } else {
+ return Promise.reject();
+ }
+ };
+
+ render() {
+ const { almApplication, almInstallId, almOrganization } = this.props;
+ if (almInstallId && almOrganization) {
+ return (
+ <OrganizationDetailsStep
+ description={
+ <p className="huge-spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_organization.import_organization_x')}
+ id="onboarding.create_organization.import_organization_x"
+ values={{
+ avatar: (
+ <img
+ alt={almApplication.name}
+ className="little-spacer-left"
+ src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
+ almApplication.key
+ )}.svg`}
+ width={16}
+ />
+ ),
+ name: <strong>{almOrganization.name}</strong>
+ }}
+ />
+ </p>
+ }
+ finished={false}
+ onContinue={this.handleCreateOrganization}
+ onOpen={() => {}}
+ open={true}
+ organization={almOrganization}
+ submitText={translate('my_account.create_organization')}
+ />
+ );
+ }
+ return (
+ <ChooseRemoteOrganizationStep
+ almApplication={this.props.almApplication}
+ almInstallId={almInstallId}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
new file mode 100644
index 00000000000..25a091e9246
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import Step from '../../tutorials/components/Step';
+import { translate } from '../../../helpers/l10n';
+import { AlmApplication } from '../../../app/types';
+
+interface Props {
+ almApplication: AlmApplication;
+ almInstallId?: string;
+}
+
+export default class ChooseRemoteOrganizationStep extends React.PureComponent<Props> {
+ renderForm = () => {
+ const { almApplication, almInstallId } = this.props;
+ return (
+ <div className="boxed-group-inner">
+ {almInstallId && (
+ <span className="alert alert-warning markdown big-spacer-bottom width-60">
+ {translate('onboarding.create_organization.import_org_not_found')}
+ <ul>
+ <li>{translate('onboarding.create_organization.import_org_not_found.tips_1')}</li>
+ <li>{translate('onboarding.create_organization.import_org_not_found.tips_2')}</li>
+ </ul>
+ </span>
+ )}
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={almApplication}
+ small={true}
+ url={almApplication.installationUrl}>
+ {translate(
+ 'onboarding.create_organization.choose_organization_button',
+ almApplication.key
+ )}
+ </IdentityProviderLink>
+ </div>
+ );
+ };
+
+ renderResult = () => {
+ return null;
+ };
+
+ render() {
+ return (
+ <Step
+ finished={false}
+ onOpen={() => {}}
+ open={true}
+ renderForm={this.renderForm}
+ renderResult={this.renderResult}
+ stepNumber={1}
+ stepTitle={translate('onboarding.create_organization.import_org_details')}
+ />
+ );
+ }
+}
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 b6c0fa25b32..2767cdfa399 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
@@ -18,17 +18,29 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
import { Helmet } from 'react-helmet';
import { FormattedMessage } from 'react-intl';
import { Link, withRouter, WithRouterProps } from 'react-router';
-import { connect } from 'react-redux';
-import { Dispatch } from 'redux';
-import OrganizationDetailsStep from './OrganizationDetailsStep';
-import PlanStep from './PlanStep';
-import { formatPrice } from './utils';
+import { formatPrice, parseQuery } from './utils';
import { whenLoggedIn } from './whenLoggedIn';
+import AutoOrganizationCreate from './AutoOrganizationCreate';
+import ManualOrganizationCreate from './ManualOrganizationCreate';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import Tabs from '../../../components/controls/Tabs';
+import { getAlmAppInfo, getAlmOrganization } from '../../../api/alm-integration';
import { getSubscriptionPlans } from '../../../api/billing';
-import { OrganizationBase, Organization, SubscriptionPlan } from '../../../app/types';
+import {
+ LoggedInUser,
+ Organization,
+ SubscriptionPlan,
+ AlmApplication,
+ AlmOrganization,
+ OrganizationBase
+} from '../../../app/types';
+import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
import { translate } from '../../../helpers/l10n';
import { getOrganizationUrl } from '../../../helpers/urls';
import * as api from '../../../api/organizations';
@@ -38,27 +50,26 @@ import '../../tutorials/styles.css'; // TODO remove me
interface Props {
createOrganization: (organization: OrganizationBase) => Promise<Organization>;
+ currentUser: LoggedInUser;
deleteOrganization: (key: string) => Promise<void>;
}
-enum Step {
- OrganizationDetails,
- Plan
-}
-
interface State {
+ almApplication?: AlmApplication;
+ almOrganization?: AlmOrganization;
loading: boolean;
organization?: Organization;
- step: Step;
subscriptionPlans?: SubscriptionPlan[];
}
+interface LocationState {
+ paid?: boolean;
+ tab?: 'auto' | 'manual';
+}
+
export class CreateOrganization extends React.PureComponent<Props & WithRouterProps, State> {
mounted = false;
- state: State = {
- loading: true,
- step: Step.OrganizationDetails
- };
+ state: State = { loading: true };
componentDidMount() {
this.mounted = true;
@@ -66,7 +77,16 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
if (document.documentElement) {
document.documentElement.classList.add('white-page');
}
- this.fetchSubscriptionPlans();
+ const initRequests = [this.fetchSubscriptionPlans()];
+ if (hasAdvancedALMIntegration(this.props.currentUser)) {
+ initRequests.push(this.fetchAlmApplication());
+
+ const query = parseQuery(this.props.location.query);
+ if (query.almInstallId) {
+ initRequests.push(this.fetchAlmOrganization(query.almInstallId));
+ }
+ }
+ Promise.all(initRequests).then(this.stopLoading, this.stopLoading);
}
componentWillUnmount() {
@@ -74,79 +94,63 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
document.body.classList.remove('white-page');
}
- fetchSubscriptionPlans = () => {
- getSubscriptionPlans().then(
- subscriptionPlans => {
- if (this.mounted) {
- this.setState({ loading: false, subscriptionPlans });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
+ fetchAlmApplication = () => {
+ return getAlmAppInfo().then(({ application }) => {
+ if (this.mounted) {
+ this.setState({ almApplication: application });
}
- );
- };
-
- finishCreation = (key: string) => {
- this.props.router.push({
- pathname: getOrganizationUrl(key),
- state: { justCreated: true }
});
};
- handleOrganizationDetailsStepOpen = () => {
- this.setState({ step: Step.OrganizationDetails });
+ fetchAlmOrganization = (installationId: string) => {
+ return getAlmOrganization({ installationId }).then(almOrganization => {
+ if (this.mounted) {
+ this.setState({ almOrganization });
+ }
+ });
};
- handleOrganizationDetailsFinish = (organization: Required<OrganizationBase>) => {
- this.setState({ organization, step: Step.Plan });
- return Promise.resolve();
+ fetchSubscriptionPlans = () => {
+ return getSubscriptionPlans().then(subscriptionPlans => {
+ if (this.mounted) {
+ this.setState({ subscriptionPlans });
+ }
+ });
};
- handlePaidPlanChoose = () => {
- if (this.state.organization) {
- this.finishCreation(this.state.organization.key);
- }
+ handleOrgCreated = (organization: string) => {
+ this.props.router.push({
+ pathname: getOrganizationUrl(organization),
+ state: { justCreated: true }
+ });
};
- handleFreePlanChoose = () => {
- return this.createOrganization().then(key => {
- this.finishCreation(key);
- });
+ onTabChange = (tab: 'auto' | 'manual') => {
+ this.updateUrl({ tab });
};
- createOrganization = () => {
- const { organization } = this.state;
- if (organization) {
- return this.props
- .createOrganization({
- avatar: organization.avatar,
- description: organization.description,
- key: organization.key,
- name: organization.name || organization.key,
- url: organization.url
- })
- .then(({ key }) => key);
- } else {
- return Promise.reject();
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
}
};
- deleteOrganization = () => {
- const { organization } = this.state;
- if (organization) {
- this.props.deleteOrganization(organization.key).catch(() => {});
- }
+ updateUrl = (state: Partial<LocationState> = {}) => {
+ this.props.router.replace({
+ pathname: this.props.location.pathname,
+ state: { ...(this.props.location.state || {}), ...state }
+ });
};
render() {
const { location } = this.props;
- const { loading, subscriptionPlans } = this.state;
+ const { almApplication, loading, subscriptionPlans } = this.state;
+ const state = (location.state || {}) as LocationState;
+ const query = parseQuery(location.query);
const header = translate('onboarding.create_organization.page.header');
const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
const formattedPrice = formatPrice(startedPrice);
+ const showManualTab = state.tab === 'manual' && !query.almInstallId;
return (
<>
@@ -174,27 +178,59 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
</header>
{loading ? (
- <i className="spinner" />
+ <DeferredSpinner />
) : (
<>
- <OrganizationDetailsStep
- finished={this.state.organization !== undefined}
- onContinue={this.handleOrganizationDetailsFinish}
- onOpen={this.handleOrganizationDetailsStepOpen}
- open={this.state.step === Step.OrganizationDetails}
- organization={this.state.organization}
- />
-
- {subscriptionPlans !== undefined && (
- <PlanStep
- createOrganization={this.createOrganization}
- deleteOrganization={this.deleteOrganization}
- onFreePlanChoose={this.handleFreePlanChoose}
- onPaidPlanChoose={this.handlePaidPlanChoose}
- onlyPaid={location.state && location.state.paid === true}
- open={this.state.step === Step.Plan}
- startingPrice={formattedPrice}
- subscriptionPlans={subscriptionPlans}
+ {almApplication && (
+ <Tabs
+ onChange={this.onTabChange}
+ selected={showManualTab ? 'manual' : 'auto'}
+ tabs={[
+ {
+ key: 'auto',
+ node: (
+ <>
+ {translate(
+ 'onboarding.create_organization.import_organization',
+ almApplication.key
+ )}
+ <span
+ className={classNames(
+ 'rounded alert alert-small spacer-left display-inline-block',
+ {
+ 'alert-info': !showManualTab,
+ 'alert-muted': showManualTab
+ }
+ )}>
+ {translate('beta')}
+ </span>
+ </>
+ )
+ },
+ {
+ disabled: Boolean(query.almInstallId),
+ key: 'manual',
+ node: translate('onboarding.create_organization.create_manually')
+ }
+ ]}
+ />
+ )}
+
+ {showManualTab || !almApplication ? (
+ <ManualOrganizationCreate
+ createOrganization={this.props.createOrganization}
+ deleteOrganization={this.props.deleteOrganization}
+ onOrgCreated={this.handleOrgCreated}
+ onlyPaid={state.paid}
+ subscriptionPlans={this.state.subscriptionPlans}
+ />
+ ) : (
+ <AutoOrganizationCreate
+ almApplication={almApplication}
+ almInstallId={query.almInstallId}
+ almOrganization={this.state.almOrganization}
+ createOrganization={this.props.createOrganization}
+ onOrgCreated={this.handleOrgCreated}
/>
)}
</>
@@ -205,7 +241,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
}
}
-function createOrganization(organization: OrganizationBase) {
+function createOrganization(organization: OrganizationBase & { installId?: string }) {
return (dispatch: Dispatch) => {
return api.createOrganization(organization).then((organization: Organization) => {
dispatch(actions.createOrganization(organization));
@@ -228,8 +264,10 @@ const mapDispatchToProps = {
};
export default whenLoggedIn(
- connect(
- null,
- mapDispatchToProps
- )(withRouter(CreateOrganization))
+ withRouter(
+ connect(
+ null,
+ mapDispatchToProps
+ )(CreateOrganization)
+ )
);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx
new file mode 100644
index 00000000000..6bf245e9088
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx
@@ -0,0 +1,133 @@
+/*
+ * 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';
+import OrganizationDetailsStep from './OrganizationDetailsStep';
+import PlanStep from './PlanStep';
+import { formatPrice } from './utils';
+import { OrganizationBase, Organization, SubscriptionPlan } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ createOrganization: (organization: OrganizationBase) => Promise<Organization>;
+ deleteOrganization: (key: string) => Promise<void>;
+ onOrgCreated: (organization: string) => void;
+ onlyPaid?: boolean;
+ subscriptionPlans?: SubscriptionPlan[];
+}
+
+enum Step {
+ OrganizationDetails,
+ Plan
+}
+
+interface State {
+ organization?: Organization;
+ step: Step;
+}
+
+export default class ManualOrganizationCreate extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { step: Step.OrganizationDetails };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleOrganizationDetailsStepOpen = () => {
+ this.setState({ step: Step.OrganizationDetails });
+ };
+
+ handleOrganizationDetailsFinish = (organization: Required<OrganizationBase>) => {
+ this.setState({ organization, step: Step.Plan });
+ return Promise.resolve();
+ };
+
+ handlePaidPlanChoose = () => {
+ if (this.state.organization) {
+ this.props.onOrgCreated(this.state.organization.key);
+ }
+ };
+
+ handleFreePlanChoose = () => {
+ return this.createOrganization().then(key => {
+ this.props.onOrgCreated(key);
+ });
+ };
+
+ createOrganization = () => {
+ const { organization } = this.state;
+ if (organization) {
+ return this.props
+ .createOrganization({
+ avatar: organization.avatar,
+ description: organization.description,
+ key: organization.key,
+ name: organization.name || organization.key,
+ url: organization.url
+ })
+ .then(({ key }) => key);
+ } else {
+ return Promise.reject();
+ }
+ };
+
+ deleteOrganization = () => {
+ const { organization } = this.state;
+ if (organization) {
+ this.props.deleteOrganization(organization.key).catch(() => {});
+ }
+ };
+
+ render() {
+ const { subscriptionPlans } = this.props;
+ const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
+ const formattedPrice = formatPrice(startedPrice);
+
+ return (
+ <>
+ <OrganizationDetailsStep
+ finished={this.state.organization !== undefined}
+ onContinue={this.handleOrganizationDetailsFinish}
+ onOpen={this.handleOrganizationDetailsStepOpen}
+ open={this.state.step === Step.OrganizationDetails}
+ organization={this.state.organization}
+ submitText={translate('continue')}
+ />
+
+ {subscriptionPlans !== undefined && (
+ <PlanStep
+ createOrganization={this.createOrganization}
+ deleteOrganization={this.deleteOrganization}
+ onFreePlanChoose={this.handleFreePlanChoose}
+ onPaidPlanChoose={this.handlePaidPlanChoose}
+ onlyPaid={this.props.onlyPaid}
+ open={this.state.step === Step.Plan}
+ startingPrice={formattedPrice}
+ subscriptionPlans={subscriptionPlans}
+ />
+ )}
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx
index 8357fa54698..9526e064562 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx
@@ -29,6 +29,7 @@ interface Props {
error: string | undefined;
id: string;
isSubmitting: boolean;
+ isValidating: boolean;
label: React.ReactNode;
name: string;
onBlur: React.FocusEventHandler;
@@ -39,12 +40,12 @@ interface Props {
}
export default function OrganizationDetailsInput(props: Props) {
- const hasError = props.dirty && props.touched && props.error !== undefined;
+ const hasError = props.dirty && props.touched && !props.isValidating && props.error !== undefined;
const isValid = props.dirty && props.touched && props.error === undefined;
return (
<div>
<label htmlFor={props.id}>
- {props.label}
+ <strong>{props.label}</strong>
{props.required && <em className="mandatory">*</em>}
</label>
<div className="little-spacer-top spacer-bottom">
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
index cf5026f87ae..75f1b75d17b 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
@@ -26,6 +26,7 @@ import { translate } from '../../../helpers/l10n';
import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import { getHostUrl } from '../../../helpers/urls';
import { OrganizationBase } from '../../../app/types';
import { getOrganization } from '../../../api/organizations';
@@ -40,11 +41,13 @@ const initialValues: Values = {
};
interface Props {
+ description?: React.ReactNode;
finished: boolean;
onContinue: (organization: Required<OrganizationBase>) => Promise<void>;
onOpen: () => void;
open: boolean;
organization?: OrganizationBase & { key: string };
+ submitText: string;
}
interface State {
@@ -118,10 +121,17 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props,
handleChange,
isSubmitting,
isValid,
+ isValidating,
touched,
values
} = props;
- const commonProps = { dirty, isSubmitting, onBlur: handleBlur, onChange: handleChange };
+ const commonProps = {
+ dirty,
+ isValidating,
+ isSubmitting,
+ onBlur: handleBlur,
+ onChange: handleChange
+ };
return (
<>
<OrganizationDetailsInput
@@ -134,7 +144,14 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props,
required={true}
touched={touched.key}
value={values.key}>
- {props => <input autoFocus={true} maxLength={255} {...props} />}
+ {props => (
+ <div className="display-inline-flex-baseline">
+ <span className="little-spacer-right">
+ {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
+ </span>
+ <input autoFocus={true} maxLength={255} {...props} />
+ </div>
+ )}
</OrganizationDetailsInput>
<div className="big-spacer-top">
<ResetButtonLink onClick={this.handleAdditionalClick}>
@@ -170,7 +187,19 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props,
name="avatar"
touched={touched.avatar && values.avatar !== ''}
value={values.avatar}>
- {props => <input {...props} />}
+ {props => (
+ <>
+ {values.avatar && (
+ <img
+ alt=""
+ className="display-block spacer-bottom rounded"
+ src={values.avatar}
+ width={48}
+ />
+ )}
+ <input {...props} />
+ </>
+ )}
</OrganizationDetailsInput>
</div>
<div className="big-spacer-top">
@@ -199,7 +228,7 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props,
</div>
</div>
<div className="big-spacer-top">
- <SubmitButton disabled={isSubmitting || !isValid}>{translate('continue')}</SubmitButton>
+ <SubmitButton disabled={isSubmitting || !isValid}>{this.props.submitText}</SubmitButton>
</div>
</>
);
@@ -208,6 +237,7 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props,
renderForm = () => {
return (
<div className="boxed-group-inner">
+ {this.props.description}
<ValidationForm<Values>
initialValues={this.getInitialValues()}
isInitialValid={this.props.organization !== undefined}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
new file mode 100644
index 00000000000..8beb62e897c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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';
+import { shallow } from 'enzyme';
+import AutoOrganizationCreate from '../AutoOrganizationCreate';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+const organization = {
+ avatar: 'http://example.com/avatar',
+ description: 'description-foo',
+ key: 'key-foo',
+ name: 'name-foo',
+ url: 'http://example.com/foo'
+};
+
+it('should render with import org button', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render prefilled and create org', async () => {
+ const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
+ const onOrgCreated = jest.fn();
+ const wrapper = shallowRender({
+ almInstallId: 'id-foo',
+ almOrganization: {
+ ...organization,
+ type: 'ORGANIZATION'
+ },
+ createOrganization,
+ onOrgCreated
+ });
+
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+ await waitAndUpdate(wrapper);
+
+ expect(createOrganization).toBeCalledWith({ ...organization, installId: 'id-foo' });
+ expect(onOrgCreated).toBeCalledWith('foo');
+});
+
+function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) {
+ return shallow(
+ <AutoOrganizationCreate
+ almApplication={{
+ backgroundColor: '#0052CC',
+ iconPath: '"/static/authbitbucket/bitbucket.svg"',
+ installationUrl: 'https://bitbucket.org/install/app',
+ key: 'bitbucket',
+ name: 'BitBucket'
+ }}
+ createOrganization={jest.fn()}
+ onOrgCreated={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
new file mode 100644
index 00000000000..a86ab8dfd11
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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';
+import { shallow } from 'enzyme';
+import ChooseRemoteOrganizationStep from '../ChooseRemoteOrganizationStep';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should display a warning message', () => {
+ expect(shallowRender({ almInstallId: 'foo' }).find('.alert-warning')).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) {
+ return shallow(
+ <ChooseRemoteOrganizationStep
+ almApplication={{
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ installationUrl: 'https://alm.application.url',
+ key: 'github',
+ name: 'GitHub'
+ }}
+ {...props}
+ />
+ ).dive();
+}
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 6adb8f9d520..9c1e58367fb 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
@@ -18,9 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { Location } from 'history';
import { shallow } from 'enzyme';
import { CreateOrganization } from '../CreateOrganization';
import { mockRouter, waitAndUpdate } from '../../../../helpers/testUtils';
+import { LoggedInUser } from '../../../../app/types';
jest.mock('../../../../api/billing', () => ({
getSubscriptionPlans: jest
@@ -28,73 +30,93 @@ jest.mock('../../../../api/billing', () => ({
.mockResolvedValue([{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }])
}));
-const organization = {
- avatar: 'http://example.com/avatar',
- description: 'description-foo',
- key: 'key-foo',
- name: 'name-foo',
- url: 'http://example.com/foo'
+jest.mock('../../../../api/alm-integration', () => ({
+ getAlmAppInfo: jest.fn().mockResolvedValue({
+ application: {
+ installationUrl: 'https://alm.installation.url',
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ key: 'github',
+ name: 'GitHub'
+ }
+ }),
+ getAlmOrganization: jest.fn().mockResolvedValue({
+ key: 'sonarsource',
+ name: 'SonarSource',
+ description: 'Continuous Code Quality',
+ url: 'https://www.sonarsource.com',
+ avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4',
+ type: 'ORGANIZATION'
+ })
+}));
+
+const user: LoggedInUser = {
+ groups: [],
+ isLoggedIn: true,
+ login: 'luke',
+ name: 'Skywalker',
+ scmAccounts: [],
+ showOnboardingTutorial: false
};
-it('should render and create organization', async () => {
- const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
- const router = mockRouter();
- const wrapper = shallow(
- // @ts-ignore avoid passing everything from WithRouterProps
- <CreateOrganization createOrganization={createOrganization} location={{}} router={router} />
- );
+it('should render with manual tab displayed', async () => {
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
+});
- wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+it('should preselect paid plan on manual creation', async () => {
+ const location = { state: { paid: true } };
+ // @ts-ignore avoid passing everything from WithRouterProps
+ const wrapper = shallowRender({ location });
await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('ManualOrganizationCreate').prop('onlyPaid')).toBe(true);
+});
- wrapper.find('PlanStep').prop<Function>('onFreePlanChoose')();
+it('should render with auto tab displayed', async () => {
+ const wrapper = shallowRender({ currentUser: { ...user, externalProvider: 'github' } });
await waitAndUpdate(wrapper);
- expect(createOrganization).toBeCalledWith(organization);
- expect(router.push).toBeCalledWith({
- pathname: '/organizations/foo',
- state: { justCreated: true }
- });
+ expect(wrapper).toMatchSnapshot();
});
-it('should preselect paid plan', async () => {
- const router = mockRouter();
- const location = { state: { paid: true } };
- const wrapper = shallow(
- // @ts-ignore avoid passing everything from WithRouterProps
- <CreateOrganization createOrganization={jest.fn()} location={location} router={router} />
- );
+it('should render with auto tab selected and manual disabled', async () => {
+ const wrapper = shallowRender({
+ currentUser: { ...user, externalProvider: 'github' },
+ location: { query: { installation_id: 'foo' } } as Location // eslint-disable-line camelcase
+ });
await waitAndUpdate(wrapper);
- wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should switch tabs', async () => {
+ const replace = jest.fn();
+ const wrapper = shallowRender({
+ currentUser: { ...user, externalProvider: 'github' },
+ router: mockRouter({ replace })
+ });
+
+ replace.mockImplementation(location => {
+ wrapper.setProps({ location }).update();
+ });
+
await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true);
+ (wrapper.find('Tabs').prop('onChange') as Function)('manual');
+ expect(wrapper.find('ManualOrganizationCreate').exists()).toBeTruthy();
+ (wrapper.find('Tabs').prop('onChange') as Function)('auto');
+ expect(wrapper.find('AutoOrganizationCreate').exists()).toBeTruthy();
});
-it('should roll back after upgrade failure', async () => {
- const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
- const deleteOrganization = jest.fn().mockResolvedValue(undefined);
- const router = mockRouter();
- const wrapper = shallow(
+function shallowRender(props: Partial<CreateOrganization['props']> = {}) {
+ return shallow(
<CreateOrganization
- createOrganization={createOrganization}
- deleteOrganization={deleteOrganization}
+ currentUser={user}
+ {...props}
// @ts-ignore avoid passing everything from WithRouterProps
location={{}}
- // @ts-ignore avoid passing everything from WithRouterProps
- router={router}
+ router={mockRouter()}
+ {...props}
/>
);
- await waitAndUpdate(wrapper);
-
- wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
- await waitAndUpdate(wrapper);
-
- wrapper.find('PlanStep').prop<Function>('createOrganization')();
- expect(createOrganization).toBeCalledWith(organization);
-
- wrapper.find('PlanStep').prop<Function>('deleteOrganization')();
- expect(deleteOrganization).toBeCalledWith(organization.key);
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx
new file mode 100644
index 00000000000..6226f8173b7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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';
+import { shallow } from 'enzyme';
+import ManualOrganizationCreate from '../ManualOrganizationCreate';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+const organization = {
+ avatar: 'http://example.com/avatar',
+ description: 'description-foo',
+ key: 'key-foo',
+ name: 'name-foo',
+ url: 'http://example.com/foo'
+};
+
+it('should render and create organization', async () => {
+ const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
+ const onOrgCreated = jest.fn();
+ const wrapper = shallowRender({ createOrganization, onOrgCreated });
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('PlanStep').prop<Function>('onFreePlanChoose')();
+ await waitAndUpdate(wrapper);
+ expect(createOrganization).toBeCalledWith(organization);
+ expect(onOrgCreated).toBeCalledWith('foo');
+});
+
+it('should preselect paid plan', async () => {
+ const wrapper = shallowRender({ onlyPaid: true });
+
+ await waitAndUpdate(wrapper);
+ wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true);
+});
+
+it('should roll back after upgrade failure', async () => {
+ const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
+ const deleteOrganization = jest.fn().mockResolvedValue(undefined);
+ const wrapper = shallowRender({ createOrganization, deleteOrganization });
+ await waitAndUpdate(wrapper);
+
+ wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+ await waitAndUpdate(wrapper);
+
+ wrapper.find('PlanStep').prop<Function>('createOrganization')();
+ expect(createOrganization).toBeCalledWith(organization);
+
+ wrapper.find('PlanStep').prop<Function>('deleteOrganization')();
+ expect(deleteOrganization).toBeCalledWith(organization.key);
+});
+
+function shallowRender(props: Partial<ManualOrganizationCreate['props']> = {}) {
+ return shallow(
+ <ManualOrganizationCreate
+ createOrganization={jest.fn()}
+ deleteOrganization={jest.fn()}
+ onOrgCreated={jest.fn()}
+ subscriptionPlans={[{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx
index 38629f87699..4ddeac5d913 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx
@@ -30,6 +30,7 @@ it('should render', () => {
error="This field is bad!"
id="field"
isSubmitting={true}
+ isValidating={false}
label="Label"
name="field"
onBlur={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
index aad43c0a619..f8748b45aec 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
@@ -38,6 +38,7 @@ it('should render form', () => {
onContinue={jest.fn()}
onOpen={jest.fn()}
open={true}
+ submitText="continue"
/>
);
expect(wrapper).toMatchSnapshot();
@@ -58,22 +59,29 @@ it('should render form', () => {
).toBe(false);
});
-it('should validate', () => {
+it('should validate', async () => {
const wrapper = shallow(
<OrganizationDetailsStep
finished={false}
onContinue={jest.fn()}
onOpen={jest.fn()}
open={true}
+ submitText="continue"
/>
);
const instance = wrapper.instance() as OrganizationDetailsStep;
- expect(
- instance.handleValidate({ avatar: '', description: '', name: '', key: 'foo', url: '' })
+ await expect(
+ instance.handleValidate({
+ avatar: '',
+ description: '',
+ name: '',
+ key: 'foo',
+ url: ''
+ })
).resolves.toEqual({});
- expect(
+ await expect(
instance.handleValidate({
avatar: '',
description: '',
@@ -81,13 +89,21 @@ it('should validate', () => {
key: 'x'.repeat(256),
url: ''
})
- ).rejects.toEqual({ key: 'onboarding.create_organization.organization_name.error' });
+ ).rejects.toEqual({
+ key: 'onboarding.create_organization.organization_name.error'
+ });
- expect(
- instance.handleValidate({ avatar: 'bla', description: '', name: '', key: 'foo', url: '' })
+ await expect(
+ instance.handleValidate({
+ avatar: 'bla',
+ description: '',
+ name: '',
+ key: 'foo',
+ url: ''
+ })
).rejects.toEqual({ avatar: 'onboarding.create_organization.avatar.error' });
- expect(
+ await expect(
instance.handleValidate({
avatar: '',
description: '',
@@ -95,16 +111,34 @@ it('should validate', () => {
key: 'foo',
url: ''
})
- ).rejects.toEqual({ name: 'onboarding.create_organization.display_name.error' });
+ ).rejects.toEqual({
+ name: 'onboarding.create_organization.display_name.error'
+ });
- expect(
- instance.handleValidate({ avatar: '', description: '', name: '', key: 'foo', url: 'bla' })
- ).rejects.toEqual({ url: 'onboarding.create_organization.url.error' });
+ await expect(
+ instance.handleValidate({
+ avatar: '',
+ description: '',
+ name: '',
+ key: 'foo',
+ url: 'bla'
+ })
+ ).rejects.toEqual({
+ url: 'onboarding.create_organization.url.error'
+ });
(getOrganization as jest.Mock).mockResolvedValue({});
- expect(
- instance.handleValidate({ avatar: '', description: '', name: '', key: 'foo', url: '' })
- ).rejects.toEqual({ key: 'onboarding.create_organization.organization_name.taken' });
+ await expect(
+ instance.handleValidate({
+ avatar: '',
+ description: '',
+ name: '',
+ key: 'foo',
+ url: ''
+ })
+ ).rejects.toEqual({
+ key: 'onboarding.create_organization.organization_name.taken'
+ });
});
it('should render result', () => {
@@ -115,6 +149,7 @@ it('should render result', () => {
onOpen={jest.fn()}
open={false}
organization={{ avatar: '', description: '', key: 'org', name: 'Organization', url: '' }}
+ submitText="continue"
/>
);
expect(wrapper.dive()).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
new file mode 100644
index 00000000000..a57042c6f50
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render prefilled and create org 1`] = `
+<OrganizationDetailsStep
+ description={
+ <p
+ className="huge-spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="onboarding.create_organization.import_organization_x"
+ id="onboarding.create_organization.import_organization_x"
+ values={
+ Object {
+ "avatar": <img
+ alt="BitBucket"
+ className="little-spacer-left"
+ src="/images/sonarcloud/bitbucket.svg"
+ width={16}
+ />,
+ "name": <strong>
+ name-foo
+ </strong>,
+ }
+ }
+ />
+ </p>
+ }
+ finished={false}
+ onContinue={[Function]}
+ onOpen={[Function]}
+ open={true}
+ organization={
+ Object {
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
+ "key": "key-foo",
+ "name": "name-foo",
+ "type": "ORGANIZATION",
+ "url": "http://example.com/foo",
+ }
+ }
+ submitText="my_account.create_organization"
+/>
+`;
+
+exports[`should render with import org button 1`] = `
+<ChooseRemoteOrganizationStep
+ almApplication={
+ Object {
+ "backgroundColor": "#0052CC",
+ "iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"",
+ "installationUrl": "https://bitbucket.org/install/app",
+ "key": "bitbucket",
+ "name": "BitBucket",
+ }
+ }
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
new file mode 100644
index 00000000000..ec99dad98ea
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display a warning message 1`] = `
+<span
+ className="alert alert-warning markdown big-spacer-bottom width-60"
+>
+ onboarding.create_organization.import_org_not_found
+ <ul>
+ <li>
+ onboarding.create_organization.import_org_not_found.tips_1
+ </li>
+ <li>
+ onboarding.create_organization.import_org_not_found.tips_2
+ </li>
+ </ul>
+</span>
+`;
+
+exports[`should render 1`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 1
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.import_org_details
+ </h2>
+ </div>
+ <div
+ className=""
+ >
+ <div
+ className="boxed-group-inner"
+ >
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.application.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ small={true}
+ url="https://alm.application.url"
+ >
+ onboarding.create_organization.choose_organization_button.github
+ </IdentityProviderLink>
+ </div>
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
index 867e8c9f4a0..f2da7e6a62c 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render and create organization 1`] = `
+exports[`should render with auto tab displayed 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
@@ -42,37 +42,47 @@ exports[`should render and create organization 1`] = `
/>
</p>
</header>
- <OrganizationDetailsStep
- finished={false}
- onContinue={[Function]}
- onOpen={[Function]}
- open={true}
- />
- <PlanStep
- createOrganization={[Function]}
- deleteOrganization={[Function]}
- onFreePlanChoose={[Function]}
- onPaidPlanChoose={[Function]}
- open={false}
- startingPrice="billing.price_format.10"
- subscriptionPlans={
+ <Tabs
+ onChange={[Function]}
+ selected="auto"
+ tabs={
Array [
Object {
- "maxNcloc": 100000,
- "price": 10,
+ "key": "auto",
+ "node": <React.Fragment>
+ onboarding.create_organization.import_organization.github
+ <span
+ className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ >
+ beta
+ </span>
+ </React.Fragment>,
},
Object {
- "maxNcloc": 250000,
- "price": 75,
+ "disabled": false,
+ "key": "manual",
+ "node": "onboarding.create_organization.create_manually",
},
]
}
/>
+ <AutoOrganizationCreate
+ almApplication={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ onOrgCreated={[Function]}
+ />
</div>
</Fragment>
`;
-exports[`should render and create organization 2`] = `
+exports[`should render with auto tab selected and manual disabled 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
@@ -114,28 +124,101 @@ exports[`should render and create organization 2`] = `
/>
</p>
</header>
- <OrganizationDetailsStep
- finished={true}
- onContinue={[Function]}
- onOpen={[Function]}
- open={false}
- organization={
+ <Tabs
+ onChange={[Function]}
+ selected="auto"
+ tabs={
+ Array [
+ Object {
+ "key": "auto",
+ "node": <React.Fragment>
+ onboarding.create_organization.import_organization.github
+ <span
+ className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ >
+ beta
+ </span>
+ </React.Fragment>,
+ },
+ Object {
+ "disabled": true,
+ "key": "manual",
+ "node": "onboarding.create_organization.create_manually",
+ },
+ ]
+ }
+ />
+ <AutoOrganizationCreate
+ almApplication={
Object {
- "avatar": "http://example.com/avatar",
- "description": "description-foo",
- "key": "key-foo",
- "name": "name-foo",
- "url": "http://example.com/foo",
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
}
}
+ almInstallId="foo"
+ almOrganization={
+ Object {
+ "avatar": "https://avatars3.githubusercontent.com/u/37629810?v=4",
+ "description": "Continuous Code Quality",
+ "key": "sonarsource",
+ "name": "SonarSource",
+ "type": "ORGANIZATION",
+ "url": "https://www.sonarsource.com",
+ }
+ }
+ onOrgCreated={[Function]}
/>
- <PlanStep
- createOrganization={[Function]}
- deleteOrganization={[Function]}
- onFreePlanChoose={[Function]}
- onPaidPlanChoose={[Function]}
- open={true}
- startingPrice="billing.price_format.10"
+ </div>
+</Fragment>
+`;
+
+exports[`should render with manual tab displayed 1`] = `
+<Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_organization.page.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title big-spacer-bottom"
+ >
+ onboarding.create_organization.page.header
+ </h1>
+ <p
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="onboarding.create_organization.page.description"
+ id="onboarding.create_organization.page.description"
+ values={
+ Object {
+ "break": <br />,
+ "more": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/sonarcloud-pricing/"
+ >
+ learn_more
+ </Link>,
+ "price": "billing.price_format.10",
+ }
+ }
+ />
+ </p>
+ </header>
+ <ManualOrganizationCreate
+ onOrgCreated={[Function]}
subscriptionPlans={
Array [
Object {
@@ -152,3 +235,85 @@ exports[`should render and create organization 2`] = `
</div>
</Fragment>
`;
+
+exports[`should switch tabs 1`] = `
+<Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_organization.page.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title big-spacer-bottom"
+ >
+ onboarding.create_organization.page.header
+ </h1>
+ <p
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="onboarding.create_organization.page.description"
+ id="onboarding.create_organization.page.description"
+ values={
+ Object {
+ "break": <br />,
+ "more": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/sonarcloud-pricing/"
+ >
+ learn_more
+ </Link>,
+ "price": "billing.price_format.10",
+ }
+ }
+ />
+ </p>
+ </header>
+ <Tabs
+ onChange={[Function]}
+ selected="auto"
+ tabs={
+ Array [
+ Object {
+ "key": "auto",
+ "node": <React.Fragment>
+ onboarding.create_organization.import_organization.github
+ <span
+ className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ >
+ beta
+ </span>
+ </React.Fragment>,
+ },
+ Object {
+ "disabled": false,
+ "key": "manual",
+ "node": "onboarding.create_organization.create_manually",
+ },
+ ]
+ }
+ />
+ <AutoOrganizationCreate
+ almApplication={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ onOrgCreated={[Function]}
+ />
+ </div>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
new file mode 100644
index 00000000000..be548a156d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render and create organization 1`] = `
+<Fragment>
+ <OrganizationDetailsStep
+ finished={false}
+ onContinue={[Function]}
+ onOpen={[Function]}
+ open={true}
+ submitText="continue"
+ />
+ <PlanStep
+ createOrganization={[Function]}
+ deleteOrganization={[Function]}
+ onFreePlanChoose={[Function]}
+ onPaidPlanChoose={[Function]}
+ open={false}
+ startingPrice="billing.price_format.10"
+ subscriptionPlans={
+ Array [
+ Object {
+ "maxNcloc": 100000,
+ "price": 10,
+ },
+ Object {
+ "maxNcloc": 250000,
+ "price": 75,
+ },
+ ]
+ }
+ />
+</Fragment>
+`;
+
+exports[`should render and create organization 2`] = `
+<Fragment>
+ <OrganizationDetailsStep
+ finished={true}
+ onContinue={[Function]}
+ onOpen={[Function]}
+ open={false}
+ organization={
+ Object {
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
+ "key": "key-foo",
+ "name": "name-foo",
+ "url": "http://example.com/foo",
+ }
+ }
+ submitText="continue"
+ />
+ <PlanStep
+ createOrganization={[Function]}
+ deleteOrganization={[Function]}
+ onFreePlanChoose={[Function]}
+ onPaidPlanChoose={[Function]}
+ open={true}
+ startingPrice="billing.price_format.10"
+ subscriptionPlans={
+ Array [
+ Object {
+ "maxNcloc": 100000,
+ "price": 10,
+ },
+ Object {
+ "maxNcloc": 250000,
+ "price": 75,
+ },
+ ]
+ }
+ />
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap
index 4142791c488..b8bd98adf5b 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap
@@ -5,7 +5,9 @@ exports[`should render 1`] = `
<label
htmlFor="field"
>
- Label
+ <strong>
+ Label
+ </strong>
<em
className="mandatory"
>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
index 76102b3bb2f..a52c598379d 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
@@ -64,6 +64,7 @@ exports[`should render form 3`] = `
dirty={false}
id="organization-key"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.organization_name"
name="key"
onBlur={[Function]}
@@ -98,6 +99,7 @@ exports[`should render form 3`] = `
dirty={false}
id="organization-display-name"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.display_name"
name="name"
onBlur={[Function]}
@@ -115,6 +117,7 @@ exports[`should render form 3`] = `
dirty={false}
id="organization-avatar"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.avatar"
name="avatar"
onBlur={[Function]}
@@ -131,6 +134,7 @@ exports[`should render form 3`] = `
dirty={false}
id="organization-description"
isSubmitting={false}
+ isValidating={false}
label="description"
name="description"
onBlur={[Function]}
@@ -147,6 +151,7 @@ exports[`should render form 3`] = `
dirty={false}
id="organization-url"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.url"
name="url"
onBlur={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/utils-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/utils-test.tsx
new file mode 100644
index 00000000000..07d1c6f16a6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/utils-test.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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 { formatPrice } from '../utils';
+
+jest.mock('../../../../helpers/urls', () => ({
+ getHostUrl: () => 'http://host.url'
+}));
+
+describe('#formatPrice', () => {
+ it('formats correctly', () => {
+ expect(formatPrice(10)).toBe('billing.price_format.10');
+ expect(formatPrice(10000)).toBe('billing.price_format.10,000');
+ expect(formatPrice(10000, true)).toBe('10,000');
+ });
+});
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 29fc906c0d7..bfe1825632a 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
@@ -17,8 +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.
*/
+import { memoize } from 'lodash';
import { translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
+import { RawQuery, parseAsOptionalString } from '../../../helpers/query';
export function formatPrice(price?: number, noSign?: boolean) {
const priceFormatted = formatMeasure(price, 'FLOAT')
@@ -26,3 +28,17 @@ export function formatPrice(price?: number, noSign?: boolean) {
.replace(/([.|,]\d)$/, '$10');
return noSign ? priceFormatted : translateWithParameters('billing.price_format', priceFormatted);
}
+
+export interface Query {
+ almInstallId?: string;
+}
+
+export const parseQuery = memoize(
+ (urlQuery: RawQuery = {}): Query => {
+ return {
+ almInstallId:
+ parseAsOptionalString(urlQuery['installation_id']) ||
+ parseAsOptionalString(urlQuery['clientKey'])
+ };
+ }
+);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx b/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
index ae6e431535d..be69f2c4361 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
@@ -20,7 +20,8 @@
import * as React from 'react';
import { withRouter, WithRouterProps } from 'react-router';
import { withCurrentUser } from './withCurrentUser';
-import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { CurrentUser } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
diff --git a/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx b/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
index ed8f05fec88..2025d4bb7f1 100644
--- a/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
@@ -20,10 +20,11 @@
import * as React from 'react';
import { connect } from 'react-redux';
import AppContainer from './components/AppContainer';
-import { CurrentUser, isLoggedIn } from '../../app/types';
+import { CurrentUser } from '../../app/types';
import { RawQuery } from '../../helpers/query';
import { getCurrentUser, Store } from '../../store/rootReducer';
import { isSonarCloud } from '../../helpers/system';
+import { isLoggedIn } from '../../helpers/users';
interface StateProps {
currentUser: CurrentUser;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
index e06f3a4fed1..6df59fe2ccb 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
@@ -21,7 +21,7 @@ import * as React from 'react';
import { pickBy, sortBy } from 'lodash';
import { searchAssignees } from '../utils';
import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
-import { Component, CurrentUser, Issue, Paging, isLoggedIn, IssueType } from '../../../app/types';
+import { Component, CurrentUser, Issue, Paging, IssueType } from '../../../app/types';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import MarkdownTips from '../../../components/common/MarkdownTips';
import SearchSelect from '../../../components/controls/SearchSelect';
@@ -35,6 +35,7 @@ import { SubmitButton } from '../../../components/ui/buttons';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Alert } from '../../../components/ui/Alert';
+import { isLoggedIn } from '../../../helpers/users';
interface AssigneeOption {
avatar?: string;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx
index e1b30839a39..1689dc06896 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx
@@ -22,7 +22,8 @@ import { connect } from 'react-redux';
import { RouterState } from 'react-router';
import { getCurrentUser, getOrganizationByKey, Store } from '../../../store/rootReducer';
import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization';
-import { Organization, CurrentUser, isLoggedIn } from '../../../app/types';
+import { Organization, CurrentUser } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
interface StateToProps {
currentUser: CurrentUser;
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 bc0bca0a822..d376085573a 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
@@ -24,6 +24,8 @@ import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import Dropdown from '../../../components/controls/Dropdown';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import OrganizationListItem from '../../../components/ui/OrganizationListItem';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import { getBaseUrl } from '../../../helpers/urls';
interface Props {
organization: Organization;
@@ -56,6 +58,21 @@ export default function OrganizationNavigationHeader({ organization, organizatio
) : (
<span className="spacer-left">{organization.name}</span>
)}
+ {organization.almRepoUrl && (
+ <a
+ className="link-no-underline"
+ href={organization.almRepoUrl}
+ rel="noopener noreferrer"
+ target="_blank">
+ <img
+ alt={sanitizeAlmId(organization.almId)}
+ className="text-text-top spacer-left"
+ height={16}
+ src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`}
+ width={16}
+ />
+ </a>
+ )}
{organization.description != null && (
<div className="navbar-context-description">
<p className="text-limited text-top" title={organization.description}>
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 57d43dc4682..021b80766e4 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
@@ -26,7 +26,20 @@ it('renders', () => {
expect(
shallow(
<OrganizationNavigationHeader
+ organization={{ key: 'foo', name: 'Foo', projectVisibility: Visibility.Public }}
+ organizations={[]}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders with alm integration', () => {
+ expect(
+ shallow(
+ <OrganizationNavigationHeader
organization={{
+ almId: 'github',
+ almRepoUrl: 'https://github.com/foo',
key: 'foo',
name: 'Foo',
projectVisibility: Visibility.Public
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 82dd13c2a2a..cf3e383e573 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
@@ -62,3 +62,40 @@ exports[`renders dropdown 1`] = `
</a>
</Dropdown>
`;
+
+exports[`renders with alm integration 1`] = `
+<header
+ className="navbar-context-header"
+>
+ <OrganizationAvatar
+ organization={
+ Object {
+ "almId": "github",
+ "almRepoUrl": "https://github.com/foo",
+ "key": "foo",
+ "name": "Foo",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <span
+ className="spacer-left"
+ >
+ Foo
+ </span>
+ <a
+ className="link-no-underline"
+ href="https://github.com/foo"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <img
+ alt="github"
+ className="text-text-top spacer-left"
+ height={16}
+ src="/images/sonarcloud/github.svg"
+ width={16}
+ />
+ </a>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/components/SonarCloudEmptyOverview.tsx b/server/sonar-web/src/main/js/apps/overview/components/SonarCloudEmptyOverview.tsx
index c72610e8991..46ebf59c072 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/SonarCloudEmptyOverview.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/SonarCloudEmptyOverview.tsx
@@ -22,9 +22,10 @@ import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import AnalyzeTutorial from '../../tutorials/analyzeProject/AnalyzeTutorial';
import MetaContainer from '../meta/MetaContainer';
-import { BranchLike, Component, CurrentUser, isLoggedIn } from '../../../app/types';
+import { BranchLike, Component, CurrentUser } from '../../../app/types';
import { isLongLivingBranch, isBranch, isMainBranch } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
+import { isLoggedIn } from '../../../helpers/users';
import { getCurrentUser, Store } from '../../../store/rootReducer';
import '../../../app/styles/sonarcloud.css';
import { Alert } from '../../../components/ui/Alert';
diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
index 79558f42221..4d55cc25215 100644
--- a/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
+++ b/server/sonar-web/src/main/js/apps/portfolio/components/Subscription.tsx
@@ -22,7 +22,8 @@ import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessI
import { ReportStatus, subscribe, unsubscribe } from '../../../api/report';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Button } from '../../../components/ui/buttons';
-import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { CurrentUser } from '../../../app/types';
+import { isLoggedIn } from '../../../helpers/users';
interface Props {
component: string;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
index cb7a50672a7..0e1c185aaac 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
@@ -26,10 +26,11 @@ import ProjectsList from './ProjectsList';
import PageSidebar from './PageSidebar';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import Visualizations from '../visualizations/Visualizations';
-import { CurrentUser, isLoggedIn, Organization } from '../../../app/types';
+import { CurrentUser, Organization } from '../../../app/types';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
import ListFooter from '../../../components/controls/ListFooter';
+import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { translate } from '../../../helpers/l10n';
import { get, save } from '../../../helpers/storage';
import { RawQuery } from '../../../helpers/query';
@@ -37,9 +38,9 @@ import { Project, Facets } from '../types';
import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils';
import { parseUrlQuery, Query, hasFilterParams, hasVisualizationParams } from '../query';
import { isSonarCloud } from '../../../helpers/system';
+import { isLoggedIn } from '../../../helpers/users';
import '../../../components/search-navigator.css';
import '../styles.css';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
export interface Props {
currentUser: CurrentUser;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
index 8b4e37b1ec6..c2d8c70efdd 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
@@ -23,8 +23,9 @@ import AllProjectsContainer from './AllProjectsContainer';
import { PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE, PROJECTS_ALL } from '../utils';
import { get } from '../../../helpers/storage';
import { searchProjects } from '../../../api/components';
-import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { CurrentUser } from '../../../app/types';
import { isSonarCloud } from '../../../helpers/system';
+import { isLoggedIn } from '../../../helpers/users';
interface Props {
currentUser: CurrentUser;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx b/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx
index 5bbcfe9d14b..42fa6f2f116 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx
@@ -21,8 +21,9 @@ import * as React from 'react';
import * as PropTypes from 'prop-types';
import { translate } from '../../../helpers/l10n';
import { Button } from '../../../components/ui/buttons';
-import { Organization, CurrentUser, isLoggedIn, hasGlobalPermission } from '../../../app/types';
+import { Organization, CurrentUser } from '../../../app/types';
import { isSonarCloud } from '../../../helpers/system';
+import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users';
interface Props {
organization?: Organization;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
index 3b9a46cbb7b..ebec0b9f090 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
@@ -20,10 +20,11 @@
import * as React from 'react';
import { IndexLink, Link } from 'react-router';
import { translate } from '../../../helpers/l10n';
-import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { CurrentUser } from '../../../app/types';
import { save } from '../../../helpers/storage';
import { RawQuery } from '../../../helpers/query';
import { PROJECTS_DEFAULT_FILTER, PROJECTS_FAVORITE, PROJECTS_ALL } from '../utils';
+import { isLoggedIn } from '../../../helpers/users';
interface Props {
currentUser: CurrentUser;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
index c29cc32f56c..54cae091589 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
@@ -23,12 +23,13 @@ import PerspectiveSelect from './PerspectiveSelect';
import ProjectsSortingSelect from './ProjectsSortingSelect';
import SearchFilterContainer from '../filters/SearchFilterContainer';
import Tooltip from '../../../components/controls/Tooltip';
-import { CurrentUser, isLoggedIn, HomePageType } from '../../../app/types';
+import { CurrentUser, HomePageType } from '../../../app/types';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../helpers/query';
import { Project } from '../types';
import { isSonarCloud } from '../../../helpers/system';
+import { isLoggedIn } from '../../../helpers/users';
interface Props {
currentUser: CurrentUser;
diff --git a/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx
index 1b1840e1381..f45dccd05d7 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx
@@ -27,14 +27,17 @@ import AutoProjectCreate from './AutoProjectCreate';
import ManualProjectCreate from './ManualProjectCreate';
import { serializeQuery, Query, parseQuery } from './utils';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import Tabs from '../../../components/controls/Tabs';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
import { getCurrentUser, Store } from '../../../store/rootReducer';
import { addGlobalErrorMessage } from '../../../store/globalMessages';
import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
-import { CurrentUser, IdentityProvider, isLoggedIn, LoggedInUser } from '../../../app/types';
+import { CurrentUser, IdentityProvider, LoggedInUser } from '../../../app/types';
import { skipOnboarding, getIdentityProviders } from '../../../api/users';
+import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
+import { isLoggedIn } from '../../../helpers/users';
import '../../../app/styles/sonarcloud.css';
interface OwnProps {
@@ -69,7 +72,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
if (query.error) {
this.props.addGlobalErrorMessage(query.error);
}
- if (!this.canAutoCreate()) {
+ if (!hasAdvancedALMIntegration(this.props.currentUser)) {
this.setState({ loading: false });
this.updateQuery({ manual: true });
} else {
@@ -102,10 +105,6 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
}
};
- canAutoCreate = ({ currentUser } = this.props) => {
- return ['bitbucket', 'github'].includes((currentUser as LoggedInUser).externalProvider || '');
- };
-
fetchIdentityProviders = () => {
getIdentityProviders().then(
({ identityProviders }) => {
@@ -127,14 +126,8 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
);
};
- showAuto = (event: React.MouseEvent<HTMLAnchorElement>) => {
- event.preventDefault();
- this.updateQuery({ manual: false });
- };
-
- showManual = (event: React.MouseEvent<HTMLAnchorElement>) => {
- event.preventDefault();
- this.updateQuery({ manual: true });
+ onTabChange = (tab: 'auto' | 'manual') => {
+ this.updateQuery({ manual: tab === 'manual' });
};
updateQuery = (changes: Partial<Query>) => {
@@ -152,49 +145,45 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
}
const { identityProvider, loading } = this.state;
- const displayManual = parseQuery(this.props.location.query).manual;
- const header = translate('onboarding.create_project.header');
- const hasAutoProvisioning = this.canAutoCreate() && identityProvider;
const query = parseQuery(this.props.location.query);
+ const header = translate('onboarding.create_project.header');
+ const hasAutoProvisioning = hasAdvancedALMIntegration(currentUser) && identityProvider;
return (
<>
<Helmet title={header} titleTemplate="%s" />
<div className="sonarcloud page page-limited">
- <div className="page-header">
+ <header className="page-header">
<h1 className="page-title">{header}</h1>
- </div>
+ </header>
{loading ? (
<DeferredSpinner />
) : (
<>
{hasAutoProvisioning && (
- <ul className="flex-tabs">
- <li>
- <a
- className={classNames('js-auto', { selected: !displayManual })}
- href="#"
- onClick={this.showAuto}>
- {translate('onboarding.create_project.select_repositories')}
- <div
- className={classNames('beta-badge spacer-left', {
- 'is-muted': displayManual
- })}>
- {translate('beta')}
- </div>
- </a>
- </li>
- <li>
- <a
- className={classNames('js-manual', { selected: displayManual })}
- href="#"
- onClick={this.showManual}>
- {translate('onboarding.create_project.create_manually')}
- </a>
- </li>
- </ul>
+ <Tabs
+ onChange={this.onTabChange}
+ selected={query.manual ? 'manual' : 'auto'}
+ tabs={[
+ {
+ key: 'auto',
+ node: (
+ <>
+ {translate('onboarding.create_project.select_repositories')}
+ <span
+ className={classNames('beta-badge spacer-left', {
+ 'is-muted': query.manual
+ })}>
+ {translate('beta')}
+ </span>
+ </>
+ )
+ },
+ { key: 'manual', node: translate('onboarding.create_project.create_manually') }
+ ]}
+ />
)}
- {displayManual || !hasAutoProvisioning || !identityProvider ? (
+ {query.manual || !hasAutoProvisioning || !identityProvider ? (
<ManualProjectCreate
currentUser={currentUser}
onProjectCreate={this.handleProjectCreate}
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx
index d20c0fd9ac8..fba7c5875c7 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx
@@ -23,7 +23,7 @@ import { Location } from 'history';
import { CreateProjectPage } from '../CreateProjectPage';
import { getIdentityProviders } from '../../../../api/users';
import { LoggedInUser } from '../../../../app/types';
-import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
jest.mock('../../../../api/users', () => ({
getIdentityProviders: jest.fn().mockResolvedValue({
@@ -70,12 +70,11 @@ it('should switch tabs', async () => {
});
await waitAndUpdate(wrapper);
-
expect(wrapper).toMatchSnapshot();
- click(wrapper.find('.js-manual'));
+ wrapper.find('Tabs').prop<Function>('onChange')('manual');
expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy();
- click(wrapper.find('.js-auto'));
+ wrapper.find('Tabs').prop<Function>('onChange')('auto');
expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
});
diff --git a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
index a1eaa7bbd49..69731b0ff8e 100644
--- a/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
@@ -11,7 +11,7 @@ exports[`should render correctly 1`] = `
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
@@ -19,7 +19,7 @@ exports[`should render correctly 1`] = `
>
onboarding.create_project.header
</h1>
- </div>
+ </header>
<DeferredSpinner
timeout={100}
/>
@@ -38,7 +38,7 @@ exports[`should render correctly 2`] = `
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
@@ -46,34 +46,30 @@ exports[`should render correctly 2`] = `
>
onboarding.create_project.header
</h1>
- </div>
- <ul
- className="flex-tabs"
- >
- <li>
- <a
- className="js-auto selected"
- href="#"
- onClick={[Function]}
- >
- onboarding.create_project.select_repositories
- <div
- className="beta-badge spacer-left"
- >
- beta
- </div>
- </a>
- </li>
- <li>
- <a
- className="js-manual"
- href="#"
- onClick={[Function]}
- >
- onboarding.create_project.create_manually
- </a>
- </li>
- </ul>
+ </header>
+ <Tabs
+ onChange={[Function]}
+ selected="auto"
+ tabs={
+ Array [
+ Object {
+ "key": "auto",
+ "node": <React.Fragment>
+ onboarding.create_project.select_repositories
+ <span
+ className="beta-badge spacer-left"
+ >
+ beta
+ </span>
+ </React.Fragment>,
+ },
+ Object {
+ "key": "manual",
+ "node": "onboarding.create_project.create_manually",
+ },
+ ]
+ }
+ />
<AutoProjectCreate
identityProvider={
Object {
@@ -100,7 +96,7 @@ exports[`should render with Manual creation only 1`] = `
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
@@ -108,7 +104,7 @@ exports[`should render with Manual creation only 1`] = `
>
onboarding.create_project.header
</h1>
- </div>
+ </header>
<Connect(ManualProjectCreate)
currentUser={
Object {
@@ -137,7 +133,7 @@ exports[`should switch tabs 1`] = `
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
@@ -145,34 +141,30 @@ exports[`should switch tabs 1`] = `
>
onboarding.create_project.header
</h1>
- </div>
- <ul
- className="flex-tabs"
- >
- <li>
- <a
- className="js-auto selected"
- href="#"
- onClick={[Function]}
- >
- onboarding.create_project.select_repositories
- <div
- className="beta-badge spacer-left"
- >
- beta
- </div>
- </a>
- </li>
- <li>
- <a
- className="js-manual"
- href="#"
- onClick={[Function]}
- >
- onboarding.create_project.create_manually
- </a>
- </li>
- </ul>
+ </header>
+ <Tabs
+ onChange={[Function]}
+ selected="auto"
+ tabs={
+ Array [
+ Object {
+ "key": "auto",
+ "node": <React.Fragment>
+ onboarding.create_project.select_repositories
+ <span
+ className="beta-badge spacer-left"
+ >
+ beta
+ </span>
+ </React.Fragment>,
+ },
+ Object {
+ "key": "manual",
+ "node": "onboarding.create_project.create_manually",
+ },
+ ]
+ }
+ />
<AutoProjectCreate
identityProvider={
Object {
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
index b41edb8e99e..60a134428aa 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
@@ -26,8 +26,9 @@ import OnboardingProjectIcon from '../../../components/icons-components/Onboardi
import OnboardingTeamIcon from '../../../components/icons-components/OnboardingTeamIcon';
import { Button, ResetButtonLink } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
-import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { CurrentUser } from '../../../app/types';
import { getCurrentUser, Store } from '../../../store/rootReducer';
+import { isLoggedIn } from '../../../helpers/users';
import '../styles.css';
interface OwnProps {
diff --git a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx
index 9068fb81f94..164f58ed467 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/projectOnboarding/ProjectOnboarding.tsx
@@ -27,11 +27,12 @@ import OrganizationStep from '../components/OrganizationStep';
import TokenStep from '../components/TokenStep';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
import { getCurrentUser, areThereCustomOrganizations, Store } from '../../../store/rootReducer';
-import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { CurrentUser } from '../../../app/types';
import { ResetButtonLink } from '../../../components/ui/buttons';
import { getProjectUrl } from '../../../helpers/urls';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { isSonarCloud } from '../../../helpers/system';
+import { isLoggedIn } from '../../../helpers/users';
import '../styles.css';
interface OwnProps {
diff --git a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
index 5be1bbc5816..fef52ae8902 100644
--- a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
+++ b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
@@ -22,10 +22,11 @@ import * as classNames from 'classnames';
import { connect } from 'react-redux';
import Tooltip from './Tooltip';
import HomeIcon from '../icons-components/HomeIcon';
-import { CurrentUser, isLoggedIn, HomePage, isSameHomePage } from '../../app/types';
+import { CurrentUser, HomePage, isSameHomePage } from '../../app/types';
import { translate } from '../../helpers/l10n';
import { getCurrentUser, Store } from '../../store/rootReducer';
import { setHomePage } from '../../store/users';
+import { isLoggedIn } from '../../helpers/users';
interface StateProps {
currentUser: CurrentUser;
diff --git a/server/sonar-web/src/main/js/components/controls/Tabs.css b/server/sonar-web/src/main/js/components/controls/Tabs.css
new file mode 100644
index 00000000000..45d8d6498bd
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/Tabs.css
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+.flex-tabs {
+ display: flex;
+ clear: left;
+ margin-bottom: calc(3 * var(--gridSize));
+ border-bottom: 1px solid var(--barBorderColor);
+ font-size: var(--mediumFontSize);
+}
+
+.flex-tabs > li > a {
+ position: relative;
+ display: block;
+ top: 1px;
+ height: 100%;
+ width: 100%;
+ box-sizing: border-box;
+ color: var(--secondFontColor);
+ font-weight: 600;
+ cursor: pointer;
+ padding-bottom: calc(1.5 * var(--gridSize));
+ border-bottom: 3px solid transparent;
+ transition: color 0.2s ease;
+}
+
+.flex-tabs > li ~ li {
+ margin-left: calc(4 * var(--gridSize));
+}
+
+.flex-tabs > li > a:hover {
+ color: var(--baseFontColor);
+}
+
+.flex-tabs > li > a.selected {
+ color: var(--blue);
+ border-bottom-color: var(--blue);
+}
+
+.flex-tabs > li > a.disabled {
+ color: var(--disableGrayText) !important;
+ cursor: not-allowed !important;
+ pointer-events: none !important;
+}
diff --git a/server/sonar-web/src/main/js/components/controls/Tabs.tsx b/server/sonar-web/src/main/js/components/controls/Tabs.tsx
new file mode 100644
index 00000000000..7678b54a2c8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/Tabs.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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';
+import * as classNames from 'classnames';
+import './Tabs.css';
+
+interface Props {
+ onChange: (tab: string) => void;
+ selected?: string;
+ tabs: Array<{ disabled?: boolean; key: string; node: React.ReactNode }>;
+}
+
+export default function Tabs({ onChange, selected, tabs }: Props) {
+ return (
+ <ul className="flex-tabs">
+ {tabs.map(tab => (
+ <Tab
+ disabled={tab.disabled}
+ key={tab.key}
+ name={tab.key}
+ onSelect={onChange}
+ selected={selected === tab.key}>
+ {tab.node}
+ </Tab>
+ ))}
+ </ul>
+ );
+}
+
+interface TabProps {
+ children: React.ReactNode;
+ disabled?: boolean;
+ name: string;
+ onSelect: (tab: string) => void;
+ selected: boolean;
+}
+
+export class Tab extends React.PureComponent<TabProps> {
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (!this.props.disabled) {
+ this.props.onSelect(this.props.name);
+ }
+ };
+
+ render() {
+ const { children, disabled, name, selected } = this.props;
+ return (
+ <li>
+ <a
+ className={classNames('js-' + name, { disabled, selected })}
+ href="#"
+ onClick={this.handleClick}>
+ {children}
+ </a>
+ </li>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Tabs-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Tabs-test.tsx
new file mode 100644
index 00000000000..72441e0f8cd
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/Tabs-test.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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';
+import { shallow } from 'enzyme';
+import Tabs, { Tab } from '../Tabs';
+import { click } from '../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ const wrapper = shallow(
+ <Tabs
+ onChange={jest.fn()}
+ selected={'bar'}
+ tabs={[{ key: 'foo', node: 'Foo' }, { key: 'bar', node: 'Bar' }]}
+ />
+ );
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should switch tabs', () => {
+ const onChange = jest.fn();
+ const wrapper = shallow(
+ <Tabs
+ onChange={onChange}
+ selected={'bar'}
+ tabs={[{ key: 'foo', node: 'Foo' }, { key: 'bar', node: 'Bar' }]}
+ />
+ );
+
+ click(shallow(wrapper.find('Tab').get(0)).find('.js-foo'));
+ expect(onChange).toBeCalledWith('foo');
+ click(shallow(wrapper.find('Tab').get(1)).find('.js-bar'));
+ expect(onChange).toBeCalledWith('bar');
+});
+
+it('should render single tab correctly', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallow(
+ <Tab name="foo" onSelect={onSelect} selected={true}>
+ <span>Foo</span>
+ </Tab>
+ );
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper.find('a'));
+ expect(onSelect).toBeCalledWith('foo');
+});
+
+it('should disable single tab', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallow(
+ <Tab disabled={true} name="foo" onSelect={onSelect} selected={true}>
+ <span>Foo</span>
+ </Tab>
+ );
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper.find('a'));
+ expect(onSelect).not.toBeCalled();
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tabs-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tabs-test.tsx.snap
new file mode 100644
index 00000000000..2db4cec05a8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tabs-test.tsx.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should disable single tab 1`] = `
+<li>
+ <a
+ className="js-foo disabled selected"
+ href="#"
+ onClick={[Function]}
+ >
+ <span>
+ Foo
+ </span>
+ </a>
+</li>
+`;
+
+exports[`should render correctly 1`] = `
+<ul
+ className="flex-tabs"
+>
+ <Tab
+ key="foo"
+ name="foo"
+ onSelect={[MockFunction]}
+ selected={false}
+ >
+ Foo
+ </Tab>
+ <Tab
+ key="bar"
+ name="bar"
+ onSelect={[MockFunction]}
+ selected={true}
+ >
+ Bar
+ </Tab>
+</ul>
+`;
+
+exports[`should render single tab correctly 1`] = `
+<li>
+ <a
+ className="js-foo selected"
+ href="#"
+ onClick={[Function]}
+ >
+ <span>
+ Foo
+ </span>
+ </a>
+</li>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.tsx
index 28bd4a38b98..6d4e9744ebd 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.tsx
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.tsx
@@ -29,8 +29,9 @@ import { searchUsers } from '../../../api/users';
import { translate } from '../../../helpers/l10n';
import { getCurrentUser, Store } from '../../../store/rootReducer';
import { DropdownOverlay } from '../../controls/Dropdown';
-import { Issue, CurrentUser, isLoggedIn, OrganizationMember } from '../../../app/types';
+import { Issue, CurrentUser, OrganizationMember } from '../../../app/types';
import { isSonarCloud } from '../../../helpers/system';
+import { isLoggedIn } from '../../../helpers/users';
interface User {
avatar?: string;
diff --git a/server/sonar-web/src/main/js/helpers/almIntegrations.ts b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
index de03cb6d315..c943f67b90e 100644
--- a/server/sonar-web/src/main/js/helpers/almIntegrations.ts
+++ b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
@@ -17,6 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { isLoggedIn } from './users';
+import { CurrentUser } from '../app/types';
+
+export function hasAdvancedALMIntegration(user: CurrentUser) {
+ return (
+ isLoggedIn(user) && (isBitbucket(user.externalProvider) || isGithub(user.externalProvider))
+ );
+}
export function isBitbucket(almId?: string) {
return almId && almId.startsWith('bitbucket');
diff --git a/server/sonar-web/src/main/js/helpers/organizations.ts b/server/sonar-web/src/main/js/helpers/organizations.ts
index 97a57c14238..2c0cdc4f373 100644
--- a/server/sonar-web/src/main/js/helpers/organizations.ts
+++ b/server/sonar-web/src/main/js/helpers/organizations.ts
@@ -17,7 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Organization, isLoggedIn, OrganizationSubscription, CurrentUser } from '../app/types';
+import { isLoggedIn } from './users';
+import { Organization, OrganizationSubscription, CurrentUser } from '../app/types';
export function isPaidOrganization(organization: Organization | undefined): boolean {
return Boolean(organization && organization.subscription === OrganizationSubscription.Paid);
diff --git a/server/sonar-web/src/main/js/helpers/users.ts b/server/sonar-web/src/main/js/helpers/users.ts
new file mode 100644
index 00000000000..77968f08e3f
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/users.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { CurrentUser, LoggedInUser } from '../app/types';
+
+export function hasGlobalPermission(user: CurrentUser, permission: string): boolean {
+ if (!user.permissions) {
+ return false;
+ }
+ return user.permissions.global.includes(permission);
+}
+
+export function isLoggedIn(user: CurrentUser): user is LoggedInUser {
+ return user.isLoggedIn;
+}
diff --git a/server/sonar-web/src/main/js/store/users.ts b/server/sonar-web/src/main/js/store/users.ts
index 98df5df543f..8440dab6e2e 100644
--- a/server/sonar-web/src/main/js/store/users.ts
+++ b/server/sonar-web/src/main/js/store/users.ts
@@ -21,16 +21,24 @@ import { uniq } from 'lodash';
import { Dispatch, combineReducers } from 'redux';
import { ActionType } from './utils/actions';
import * as api from '../api/users';
-import { CurrentUser, HomePage, isLoggedIn, LoggedInUser } from '../app/types';
+import { CurrentUser, HomePage, LoggedInUser } from '../app/types';
+import { isLoggedIn } from '../helpers/users';
export function receiveCurrentUser(user: CurrentUser) {
return { type: 'RECEIVE_CURRENT_USER', user };
}
-export function skipOnboarding() {
+function skipOnboardingAction() {
return { type: 'SKIP_ONBOARDING' };
}
+export function skipOnboarding() {
+ return (dispatch: Dispatch) =>
+ api
+ .skipOnboarding()
+ .then(() => dispatch(skipOnboardingAction()), () => dispatch(skipOnboardingAction()));
+}
+
function setHomePageAction(homepage: HomePage) {
return { type: 'SET_HOMEPAGE', homepage };
}
@@ -48,7 +56,7 @@ export function setHomePage(homepage: HomePage) {
type Action =
| ActionType<typeof receiveCurrentUser, 'RECEIVE_CURRENT_USER'>
- | ActionType<typeof skipOnboarding, 'SKIP_ONBOARDING'>
+ | ActionType<typeof skipOnboardingAction, 'SKIP_ONBOARDING'>
| ActionType<typeof setHomePageAction, 'SET_HOMEPAGE'>;
export interface State {