* 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_infotags/7.5
@@ -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(); | |||
} |
@@ -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, |
@@ -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 |
@@ -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(); | |||
@@ -94,6 +93,16 @@ public class AlmAppInstallDaoTest { | |||
underTest.selectByOwner(dbSession, GITHUB, EMPTY_STRING); | |||
} | |||
@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) |
@@ -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 { |
@@ -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); | |||
} | |||
} | |||
/** |
@@ -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; |
@@ -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); | |||
} | |||
@@ -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; |
@@ -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( |
@@ -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 { |
@@ -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'); |
@@ -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'>; |
@@ -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'>; |
@@ -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 }; |
@@ -72,7 +72,7 @@ | |||
} | |||
.menu > li > a.disabled { | |||
color: #bbb !important; | |||
color: var(--disableGrayText) !important; | |||
cursor: not-allowed !important; | |||
pointer-events: none !important; | |||
} |
@@ -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; | |||
} |
@@ -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 { |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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'; | |||
@@ -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'; | |||
@@ -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'; | |||
@@ -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 = [ |
@@ -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'; | |||
@@ -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'; | |||
@@ -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; |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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')} | |||
/> | |||
); | |||
} | |||
} |
@@ -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) | |||
) | |||
); |
@@ -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} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -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"> |
@@ -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} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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(); | |||
} |
@@ -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); | |||
}); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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()} |
@@ -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(); |
@@ -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", | |||
} | |||
} | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -5,7 +5,9 @@ exports[`should render 1`] = ` | |||
<label | |||
htmlFor="field" | |||
> | |||
Label | |||
<strong> | |||
Label | |||
</strong> | |||
<em | |||
className="mandatory" | |||
> |
@@ -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]} |
@@ -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'); | |||
}); | |||
}); |
@@ -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']) | |||
}; | |||
} | |||
); |
@@ -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'; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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}> |
@@ -23,10 +23,23 @@ import OrganizationNavigationHeader from '../OrganizationNavigationHeader'; | |||
import { Visibility } from '../../../../app/types'; | |||
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 |
@@ -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> | |||
`; |
@@ -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'; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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} |
@@ -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(); | |||
}); | |||
@@ -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 { |
@@ -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 { |
@@ -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 { |
@@ -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; |
@@ -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; | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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; |
@@ -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'); |
@@ -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); |
@@ -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; | |||
} |
@@ -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 { |
@@ -2730,7 +2730,7 @@ onboarding.create_project.select_repositories=Select repositories | |||
onboarding.create_organization.page.header=Create Organization | |||
onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects.{break}To analyze a private project you must subscribe your organization to a paid plan. From {price} a month. {more} | |||
onboarding.create_organization.organization_name=Organization Name | |||
onboarding.create_organization.organization_name=Key | |||
onboarding.create_organization.organization_name.description=Up to 255 characters. All chars must be lower-case letters (a to z), digits or dash (but dash can neither be trailing nor heading). The display name can be specified in the additional info. | |||
onboarding.create_organization.organization_name.error=The provided value doesn't match the expected format. | |||
onboarding.create_organization.organization_name.taken=This name is already taken. | |||
@@ -2746,6 +2746,16 @@ onboarding.create_organization.url=URL | |||
onboarding.create_organization.url.error=The value must be a valid url. | |||
onboarding.create_organization.description=Description | |||
onboarding.create_organization.enter_org_details=Enter your organization details | |||
onboarding.create_organization.create_manually=Create manually | |||
onboarding.create_organization.import_organization.bitbucket=Import from BitBucket teams | |||
onboarding.create_organization.import_organization.github=Import from GitHub organizations | |||
onboarding.create_organization.import_organization_x=Import {avatar} {name} into SonarCloud organization | |||
onboarding.create_organization.import_org_details=Import organization details | |||
onboarding.create_organization.import_org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue: | |||
onboarding.create_organization.import_org_not_found.tips_1=You must be an administrator of the organization | |||
onboarding.create_organization.import_org_not_found.tips_2=Try to uninstall and re-install the SonarCloud App (using the button bellow) | |||
onboarding.create_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket | |||
onboarding.create_organization.choose_organization_button.github=Choose an organization on GitHub | |||
onboarding.create_organization.enter_payment_details=Enter payment details | |||
onboarding.create_organization.choose_plan=Choose a plan | |||
onboarding.create_organization.enter_your_coupon=Enter your coupon |