* Create api/alm_integration/show_organization and handle only GitHub installation
* Add import from ALM tab in Create Org page
* Do not show error while validating detail input
* Add step to create organization from ALM
* Display a warning if the installation id was not found
* Add Alm link to remote organization in org context
* Create GET api/alm_integration/show_app_info
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();
}
@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,
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
assertThat(underTest.selectByOwner(dbSession, BITBUCKETCLOUD, A_OWNER)).isEmpty();
}
-
@Test
public void selectByOwner_throws_NPE_when_alm_is_null() {
expectAlmNPE();
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();
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)
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);
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 {
*/
package org.sonar.server.user;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
* 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);
+ }
}
/**
* 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;
}
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> {
);
}
-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);
}
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;
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';
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(
BranchLike,
Component,
CurrentUser,
- isLoggedIn,
HomePageType,
HomePage,
Measure
isPullRequest
} from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
+import { isLoggedIn } from '../../../../helpers/users';
import { getCurrentUser, Store } from '../../../../store/rootReducer';
interface StateProps {
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');
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'>;
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'>;
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 };
}
.menu > li > a.disabled {
- color: #bbb !important;
+ color: var(--disableGrayText) !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
}
.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;
}
}
.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 {
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;
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;
// 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;
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;
}
export interface Organization extends OrganizationBase {
+ almId?: string;
+ almRepoUrl?: string;
adminPages?: Extension[];
canAdmin?: boolean;
canDelete?: boolean;
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';
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';
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';
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 = [
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';
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';
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;
--- /dev/null
+/*
+ * 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}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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')}
+ />
+ );
+ }
+}
* 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';
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;
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() {
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 (
<>
</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}
/>
)}
</>
}
}
-function createOrganization(organization: OrganizationBase) {
+function createOrganization(organization: OrganizationBase & { installId?: string }) {
return (dispatch: Dispatch) => {
return api.createOrganization(organization).then((organization: Organization) => {
dispatch(actions.createOrganization(organization));
};
export default whenLoggedIn(
- connect(
- null,
- mapDispatchToProps
- )(withRouter(CreateOrganization))
+ withRouter(
+ connect(
+ null,
+ mapDispatchToProps
+ )(CreateOrganization)
+ )
);
--- /dev/null
+/*
+ * 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}
+ />
+ )}
+ </>
+ );
+ }
+}
error: string | undefined;
id: string;
isSubmitting: boolean;
+ isValidating: boolean;
label: React.ReactNode;
name: string;
onBlur: React.FocusEventHandler;
}
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">
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';
};
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 {
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
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}>
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">
</div>
</div>
<div className="big-spacer-top">
- <SubmitButton disabled={isSubmitting || !isValid}>{translate('continue')}</SubmitButton>
+ <SubmitButton disabled={isSubmitting || !isValid}>{this.props.submitText}</SubmitButton>
</div>
</>
);
renderForm = () => {
return (
<div className="boxed-group-inner">
+ {this.props.description}
<ValidationForm<Values>
initialValues={this.getInitialValues()}
isInitialValid={this.props.organization !== undefined}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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();
+}
* 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
.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);
-});
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
error="This field is bad!"
id="field"
isSubmitting={true}
+ isValidating={false}
label="Label"
name="field"
onBlur={jest.fn()}
onContinue={jest.fn()}
onOpen={jest.fn()}
open={true}
+ submitText="continue"
/>
);
expect(wrapper).toMatchSnapshot();
).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: '',
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: '',
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', () => {
onOpen={jest.fn()}
open={false}
organization={{ avatar: '', description: '', key: 'org', name: 'Organization', url: '' }}
+ submitText="continue"
/>
);
expect(wrapper.dive()).toMatchSnapshot();
--- /dev/null
+// 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",
+ }
+ }
+/>
+`;
--- /dev/null
+// 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>
+`;
// 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}
/>
</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}
/>
</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 {
</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>
+`;
--- /dev/null
+// 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>
+`;
<label
htmlFor="field"
>
- Label
+ <strong>
+ Label
+ </strong>
<em
className="mandatory"
>
dirty={false}
id="organization-key"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.organization_name"
name="key"
onBlur={[Function]}
dirty={false}
id="organization-display-name"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.display_name"
name="name"
onBlur={[Function]}
dirty={false}
id="organization-avatar"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.avatar"
name="avatar"
onBlur={[Function]}
dirty={false}
id="organization-description"
isSubmitting={false}
+ isValidating={false}
label="description"
name="description"
onBlur={[Function]}
dirty={false}
id="organization-url"
isSubmitting={false}
+ isValidating={false}
label="onboarding.create_organization.url"
name="url"
onBlur={[Function]}
--- /dev/null
+/*
+ * 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');
+ });
+});
* 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')
.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'])
+ };
+ }
+);
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';
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;
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';
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;
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;
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;
) : (
<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}>
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
</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>
+`;
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';
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;
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';
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;
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;
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;
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;
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;
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 {
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 {
}
};
- canAutoCreate = ({ currentUser } = this.props) => {
- return ['bitbucket', 'github'].includes((currentUser as LoggedInUser).externalProvider || '');
- };
-
fetchIdentityProviders = () => {
getIdentityProviders().then(
({ identityProviders }) => {
);
};
- 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>) => {
}
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}
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({
});
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();
});
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
>
onboarding.create_project.header
</h1>
- </div>
+ </header>
<DeferredSpinner
timeout={100}
/>
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
>
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 {
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
>
onboarding.create_project.header
</h1>
- </div>
+ </header>
<Connect(ManualProjectCreate)
currentUser={
Object {
<div
className="sonarcloud page page-limited"
>
- <div
+ <header
className="page-header"
>
<h1
>
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 {
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 {
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 {
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;
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+// 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>
+`;
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;
* 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');
* 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);
--- /dev/null
+/*
+ * 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;
+}
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 };
}
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 {
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.
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