aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2019-02-05 09:38:19 +0100
committersonartech <sonartech@sonarsource.com>2019-03-06 11:30:41 +0100
commit9bd42df0365a3c64161ac9283c62b4a9f422402d (patch)
tree0bf09e9422342aa91c949c62481841481aec48db /server/sonar-web/src/main
parent2847ce4e648a167335d675a3ba6e71b7b1b0f248 (diff)
downloadsonarqube-9bd42df0365a3c64161ac9283c62b4a9f422402d.tar.gz
sonarqube-9bd42df0365a3c64161ac9283c62b4a9f422402d.zip
SONARCLOUD-379 Enable users sync on existing ALM bound organizations
* Display org sync advertisement block * Add membersSync prop to organization type and update mock functions * Extract RadioCard from CardPlan * Allow to customize Modal through ConfirmButton * Add user sync configuration modal * Show help tooltip when user sync is activated
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/api/organizations.ts9
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/app/components/notifications/notifications.css7
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/badges.css8
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/modals.css4
-rw-r--r--server/sonar-web/src/main/js/app/theme.js1
-rw-r--r--server/sonar-web/src/main/js/app/types.d.ts13
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx157
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/FreeCardPlan.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/PaidCardPlan.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/FreeCardPlan-test.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/PaidCardPlan-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap267
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/FreeCardPlan-test.tsx.snap101
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/PaidCardPlan-test.tsx.snap38
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/AlmApplicationInstalling-test.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AlmApplicationInstalling-test.tsx.snap49
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarCloud-test.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap9
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarCloud-test.tsx.snap13
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap21
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx121
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx169
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx55
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx93
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap69
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap156
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap143
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap271
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx27
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/controls/Modal.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/controls/RadioCard.css (renamed from server/sonar-web/src/main/js/apps/create/components/CardPlan.css)40
-rw-r--r--server/sonar-web/src/main/js/components/controls/RadioCard.tsx77
-rw-r--r--server/sonar-web/src/main/js/components/controls/SimpleModal.tsx9
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx (renamed from server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx)38
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap128
-rw-r--r--server/sonar-web/src/main/js/components/hoc/utils.ts2
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/ui/NewInfoBox.css32
-rw-r--r--server/sonar-web/src/main/js/components/ui/NewInfoBox.tsx54
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/NewInfoBox-test.tsx44
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewInfoBox-test.tsx.snap44
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts18
-rw-r--r--server/sonar-web/src/main/js/helpers/almIntegrations.ts10
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts27
66 files changed, 2116 insertions, 818 deletions
diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts
index 736dc934d39..cc2d8c0f205 100644
--- a/server/sonar-web/src/main/js/api/organizations.ts
+++ b/server/sonar-web/src/main/js/api/organizations.ts
@@ -39,6 +39,7 @@ export function getOrganization(key: string): Promise<T.Organization | undefined
interface GetOrganizationNavigation {
adminPages: T.Extension[];
+ alm?: { key: string; membersSync: boolean; url: string };
canUpdateProjectsVisibilityToPrivate: boolean;
isDefault: boolean;
pages: T.Extension[];
@@ -102,3 +103,11 @@ export interface OrganizationBilling {
export function getOrganizationBilling(organization: string): Promise<OrganizationBilling> {
return getJSON('/api/billing/show', { organization, p: 1, ps: 1 });
}
+
+export function setOrganizationMemberSync(data: { enabled: boolean; organization: string }) {
+ return post('/api/organizations/set_members_sync', data).catch(throwGlobalError);
+}
+
+export function syncMembers(organization: string) {
+ return post('/api/organizations/sync_members', { organization }).catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
index 2c1c50da27d..957a0f78ce9 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
+++ b/server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
@@ -71,7 +71,7 @@ export default class NavLatestNotification extends React.PureComponent<Props> {
<>
<li className="navbar-latest-notification" onClick={this.props.onClick}>
<div className="navbar-latest-notification-wrapper">
- <span className="badge">{translate('new')}</span>
+ <span className="badge badge-new">{translate('new')}</span>
<span className="label">{lastNews.notification}</span>
</div>
</li>
diff --git a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
index 5e2bc920858..83cb55464a0 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
@@ -10,7 +10,7 @@ exports[`should render correctly if there are new features, and the user has not
className="navbar-latest-notification-wrapper"
>
<span
- className="badge"
+ className="badge badge-new"
>
new
</span>
diff --git a/server/sonar-web/src/main/js/app/components/notifications/notifications.css b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
index 0f2209127e4..e1760937ab2 100644
--- a/server/sonar-web/src/main/js/app/components/notifications/notifications.css
+++ b/server/sonar-web/src/main/js/app/components/notifications/notifications.css
@@ -46,16 +46,11 @@
color: var(--sonarcloudBlack300);
}
-.navbar-latest-notification-wrapper .badge {
+.navbar-latest-notification-wrapper .badge-new {
position: absolute;
- height: 18px;
margin-right: var(--gridSize);
left: calc(var(--gridSize) / 2);
top: 5px;
- font-size: var(--verySmallFontSize);
- text-transform: uppercase;
- background-color: var(--lightBlue);
- color: var(--darkBlue);
}
.navbar-latest-notification-wrapper .label {
diff --git a/server/sonar-web/src/main/js/app/styles/components/badges.css b/server/sonar-web/src/main/js/app/styles/components/badges.css
index 54a16858026..cdd025107ec 100644
--- a/server/sonar-web/src/main/js/app/styles/components/badges.css
+++ b/server/sonar-web/src/main/js/app/styles/components/badges.css
@@ -80,6 +80,14 @@ a.badge {
line-height: calc(var(--tinyControlHeight) - 1px) !important;
}
+.badge-new {
+ height: 18px;
+ font-size: var(--verySmallFontSize);
+ text-transform: uppercase;
+ background-color: var(--lightBlue);
+ color: var(--darkBlue);
+}
+
.badge-muted {
background-color: transparent;
color: var(--secondFontColor);
diff --git a/server/sonar-web/src/main/js/app/styles/components/modals.css b/server/sonar-web/src/main/js/app/styles/components/modals.css
index e1f7c4e100d..7a1b8a4e72b 100644
--- a/server/sonar-web/src/main/js/app/styles/components/modals.css
+++ b/server/sonar-web/src/main/js/app/styles/components/modals.css
@@ -42,8 +42,8 @@
}
.modal-medium {
- width: 800px;
- margin-left: -400px;
+ width: 830px;
+ margin-left: -415px;
}
.modal-large {
diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js
index 322b8c11461..25ce5c25956 100644
--- a/server/sonar-web/src/main/js/app/theme.js
+++ b/server/sonar-web/src/main/js/app/theme.js
@@ -24,6 +24,7 @@ const grid = 8;
module.exports = {
// colors
blue: '#4b9fd5',
+ veryLightBlue: '#f2faff',
lightBlue: '#cae3f2',
darkBlue: '#236a97',
green: '#00aa00',
diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts
index e51039b9405..9f70f1e0f97 100644
--- a/server/sonar-web/src/main/js/app/types.d.ts
+++ b/server/sonar-web/src/main/js/app/types.d.ts
@@ -215,7 +215,10 @@ declare namespace T {
value: string;
}
- type CurrentUserSettingNames = 'notifications.optOut' | 'notifications.readDate';
+ type CurrentUserSettingNames =
+ | 'notifications.optOut'
+ | 'notifications.readDate'
+ | 'organizations.members.dismissSyncNotif';
export interface CustomMeasure {
createdAt?: string;
@@ -499,7 +502,7 @@ declare namespace T {
export interface Organization extends OrganizationBase {
actions?: OrganizationActions;
- alm?: { key: string; url: string };
+ alm?: OrganizationAlm;
adminPages?: Extension[];
canUpdateProjectsVisibilityToPrivate?: boolean;
guarded?: boolean;
@@ -510,6 +513,12 @@ declare namespace T {
subscription?: OrganizationSubscription;
}
+ export interface OrganizationAlm {
+ key: string;
+ membersSync: boolean;
+ url: string;
+ }
+
export interface OrganizationBase {
avatar?: string;
description?: string;
diff --git a/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx b/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx
deleted file mode 100644
index 6ffb21de6d5..00000000000
--- a/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import * as classNames from 'classnames';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router';
-import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages';
-import RecommendedIcon from '../../../components/icons-components/RecommendedIcon';
-import { Alert } from '../../../components/ui/Alert';
-import { formatPrice } from '../organization/utils';
-import { translate } from '../../../helpers/l10n';
-import './CardPlan.css';
-
-interface Props {
- className?: string;
- disabled?: boolean;
- onClick?: () => void;
- selected?: boolean;
- startingPrice?: number;
-}
-
-interface CardProps extends Props {
- children: React.ReactNode;
- recommended?: string;
- title: string;
-}
-
-export default function CardPlan(props: CardProps) {
- const { className, disabled, onClick, recommended, selected, startingPrice } = props;
- const isActionable = Boolean(onClick);
- return (
- <div
- aria-checked={selected}
- className={classNames(
- 'card-plan',
- { 'card-plan-actionable': isActionable, disabled, selected },
- className
- )}
- onClick={isActionable && !disabled ? onClick : undefined}
- role="radio"
- tabIndex={0}>
- <h2 className="card-plan-header big-spacer-bottom">
- <span className="display-flex-center">
- {isActionable && (
- <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
- )}
- {props.title}
- </span>
- {startingPrice !== undefined &&
- (startingPrice ? (
- <FormattedMessage
- defaultMessage={translate('billing.price_from_x')}
- id="billing.price_from_x"
- values={{
- price: <span className="card-plan-price">{formatPrice(startingPrice)}</span>
- }}
- />
- ) : (
- <span className="card-plan-price">{formatPrice(0)}</span>
- ))}
- </h2>
- <div className="card-plan-body">{props.children}</div>
- {recommended && (
- <div className="card-plan-recommended">
- <RecommendedIcon className="spacer-right" />
- <FormattedMessage
- defaultMessage={recommended}
- id={recommended}
- values={{ recommended: <strong>{translate('recommended')}</strong> }}
- />
- </div>
- )}
- </div>
- );
-}
-
-interface FreeProps extends Props {
- almName?: string;
- hasWarning: boolean;
-}
-
-export function FreeCardPlan({ almName, hasWarning, ...props }: FreeProps) {
- const showInfo = almName && props.disabled;
- const showWarning = almName && hasWarning && !props.disabled;
-
- return (
- <CardPlan startingPrice={0} title={translate('billing.free_plan.title')} {...props}>
- <>
- <div className="spacer-left">
- <ul className="big-spacer-left note">
- <li className="little-spacer-bottom">
- {translate('billing.free_plan.all_projects_analyzed_public')}
- </li>
- <li>{translate('billing.free_plan.anyone_can_browse_source_code')}</li>
- </ul>
- </div>
- {showWarning && (
- <Alert variant="warning">
- <FormattedMessage
- defaultMessage={translate('billing.free_plan.private_repo_warning')}
- id="billing.free_plan.private_repo_warning"
- values={{ alm: almName }}
- />
- </Alert>
- )}
- {showInfo && (
- <Alert variant="info">
- <FormattedMessage
- defaultMessage={translate('billing.free_plan.not_available_info')}
- id="billing.free_plan.not_available_info"
- values={{ alm: almName }}
- />
- </Alert>
- )}
- </>
- </CardPlan>
- );
-}
-
-interface PaidProps extends Props {
- isRecommended: boolean;
-}
-
-export function PaidCardPlan({ isRecommended, ...props }: PaidProps) {
- return (
- <CardPlan
- recommended={isRecommended ? translate('billing.paid_plan.recommended') : undefined}
- title={translate('billing.paid_plan.title')}
- {...props}>
- <>
- <UpgradeOrganizationAdvantages />
- <div className="big-spacer-left">
- <Link className="spacer-left" target="_blank" to="/about/pricing">
- {translate('billing.pricing.learn_more')}
- </Link>
- </div>
- </>
- </CardPlan>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/create/components/FreeCardPlan.tsx b/server/sonar-web/src/main/js/apps/create/components/FreeCardPlan.tsx
new file mode 100644
index 00000000000..494220fe28b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/FreeCardPlan.tsx
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import RadioCard, { RadioCardProps } from '../../../components/controls/RadioCard';
+import { Alert } from '../../../components/ui/Alert';
+import { formatPrice } from '../organization/utils';
+import { translate } from '../../../helpers/l10n';
+
+interface Props extends RadioCardProps {
+ almName?: string;
+ hasWarning: boolean;
+}
+
+export default function FreeCardPlan({ almName, hasWarning, ...props }: Props) {
+ const showInfo = almName && props.disabled;
+ const showWarning = almName && hasWarning && !props.disabled;
+
+ return (
+ <RadioCard title={translate('billing.free_plan.title')} titleInfo={formatPrice(0)} {...props}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ <li className="little-spacer-bottom">
+ {translate('billing.free_plan.all_projects_analyzed_public')}
+ </li>
+ <li>{translate('billing.free_plan.anyone_can_browse_source_code')}</li>
+ </ul>
+ </div>
+ {showWarning && (
+ <Alert variant="warning">
+ <FormattedMessage
+ defaultMessage={translate('billing.free_plan.private_repo_warning')}
+ id="billing.free_plan.private_repo_warning"
+ values={{ alm: almName }}
+ />
+ </Alert>
+ )}
+ {showInfo && (
+ <Alert variant="info">
+ <FormattedMessage
+ defaultMessage={translate('billing.free_plan.not_available_info')}
+ id="billing.free_plan.not_available_info"
+ values={{ alm: almName }}
+ />
+ </Alert>
+ )}
+ </RadioCard>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/components/PaidCardPlan.tsx b/server/sonar-web/src/main/js/apps/create/components/PaidCardPlan.tsx
new file mode 100644
index 00000000000..2cc3d59e5bd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/PaidCardPlan.tsx
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages';
+import RadioCard, { RadioCardProps } from '../../../components/controls/RadioCard';
+import { formatPrice } from '../organization/utils';
+import { translate } from '../../../helpers/l10n';
+
+interface Props extends RadioCardProps {
+ isRecommended: boolean;
+ startingPrice?: number;
+}
+
+export default function PaidCardPlan({ isRecommended, startingPrice, ...props }: Props) {
+ return (
+ <RadioCard
+ recommended={isRecommended ? translate('billing.paid_plan.recommended') : undefined}
+ title={translate('billing.paid_plan.title')}
+ titleInfo={
+ startingPrice !== undefined && (
+ <FormattedMessage
+ defaultMessage={translate('billing.price_from_x')}
+ id="billing.price_from_x"
+ values={{
+ price: <span className="big">{formatPrice(startingPrice)}</span>
+ }}
+ />
+ )
+ }
+ {...props}>
+ <UpgradeOrganizationAdvantages />
+ <div className="big-spacer-left">
+ <Link className="spacer-left" target="_blank" to="/about/pricing">
+ {translate('billing.pricing.learn_more')}
+ </Link>
+ </div>
+ </RadioCard>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx
index b139c12ee6a..bf54a734288 100644
--- a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx
+++ b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx
@@ -18,13 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages';
import UpgradeOrganizationModal from './UpgradeOrganizationModal';
-import CardPlan from './CardPlan';
+import RadioCard from '../../../components/controls/RadioCard';
import { Button } from '../../../components/ui/buttons';
-import { translate, hasMessage } from '../../../helpers/l10n';
+import { formatPrice } from '../organization/utils';
import { getSubscriptionPlans } from '../../../api/billing';
+import { translate, hasMessage } from '../../../helpers/l10n';
interface Props {
className?: string;
@@ -84,10 +86,20 @@ export default class UpgradeOrganizationBox extends React.PureComponent<Props, S
return (
<>
- <CardPlan
+ <RadioCard
className={this.props.className}
- startingPrice={startingPrice}
- title={translate('billing.upgrade_box.header')}>
+ title={translate('billing.upgrade_box.header')}
+ titleInfo={
+ startingPrice !== undefined && (
+ <FormattedMessage
+ defaultMessage={translate('billing.price_from_x')}
+ id="billing.price_from_x"
+ values={{
+ price: <span className="big">{formatPrice(startingPrice)}</span>
+ }}
+ />
+ )
+ }>
<>
<UpgradeOrganizationAdvantages />
<div className="big-spacer-left">
@@ -99,7 +111,7 @@ export default class UpgradeOrganizationBox extends React.PureComponent<Props, S
</Link>
</div>
</>
- </CardPlan>
+ </RadioCard>
{upgradeOrganizationModal && (
<UpgradeOrganizationModal
insideModal={this.props.insideModal}
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/FreeCardPlan-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/FreeCardPlan-test.tsx
new file mode 100644
index 00000000000..3c4cfec0d48
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/FreeCardPlan-test.tsx
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import FreeCardPlan from '../FreeCardPlan';
+
+it('should render', () => {
+ expect(shallow(<FreeCardPlan hasWarning={false} />)).toMatchSnapshot();
+});
+
+it('should render with warning', () => {
+ expect(
+ shallow(<FreeCardPlan almName="GitHub" hasWarning={true} selected={true} />)
+ ).toMatchSnapshot();
+});
+
+it('should render disabled with info', () => {
+ expect(
+ shallow(<FreeCardPlan almName="GitHub" disabled={true} hasWarning={false} />)
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx
index fb5d1e46245..6d607b0117d 100644
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx
@@ -20,11 +20,9 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationSelect, { getOptionRenderer } from '../OrganizationSelect';
+import { mockOrganization, mockOrganizationWithAlm } from '../../../../helpers/testMocks';
-const organizations = [
- { key: 'foo', name: 'Foo' },
- { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
-];
+const organizations = [mockOrganization(), mockOrganizationWithAlm({ key: 'bar', name: 'Bar' })];
it('should render correctly', () => {
expect(
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/PaidCardPlan-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/PaidCardPlan-test.tsx
new file mode 100644
index 00000000000..b775c18aca5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/PaidCardPlan-test.tsx
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import PaidCardPlan from '../PaidCardPlan';
+
+it('should render correctly', () => {
+ expect(shallow(<PaidCardPlan isRecommended={true} startingPrice={10} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap
deleted file mode 100644
index f98ce7f4c44..00000000000
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap
+++ /dev/null
@@ -1,267 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`#FreeCardPlan should render 1`] = `
-<CardPlan
- startingPrice={0}
- title="billing.free_plan.title"
->
- <div
- className="spacer-left"
- >
- <ul
- className="big-spacer-left note"
- >
- <li
- className="little-spacer-bottom"
- >
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
- </div>
-</CardPlan>
-`;
-
-exports[`#FreeCardPlan should render disabled with info 1`] = `
-<CardPlan
- disabled={true}
- startingPrice={0}
- title="billing.free_plan.title"
->
- <div
- className="spacer-left"
- >
- <ul
- className="big-spacer-left note"
- >
- <li
- className="little-spacer-bottom"
- >
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
- </div>
- <Alert
- variant="info"
- >
- <FormattedMessage
- defaultMessage="billing.free_plan.not_available_info"
- id="billing.free_plan.not_available_info"
- values={
- Object {
- "alm": "GitHub",
- }
- }
- />
- </Alert>
-</CardPlan>
-`;
-
-exports[`#FreeCardPlan should render with warning 1`] = `
-<CardPlan
- selected={true}
- startingPrice={0}
- title="billing.free_plan.title"
->
- <div
- className="spacer-left"
- >
- <ul
- className="big-spacer-left note"
- >
- <li
- className="little-spacer-bottom"
- >
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
- </div>
- <Alert
- variant="warning"
- >
- <FormattedMessage
- defaultMessage="billing.free_plan.private_repo_warning"
- id="billing.free_plan.private_repo_warning"
- values={
- Object {
- "alm": "GitHub",
- }
- }
- />
- </Alert>
-</CardPlan>
-`;
-
-exports[`#PaidCardPlan should render 1`] = `
-<CardPlan
- recommended="billing.paid_plan.recommended"
- startingPrice={10}
- title="billing.paid_plan.title"
->
- <UpgradeOrganizationAdvantages />
- <div
- className="big-spacer-left"
- >
- <Link
- className="spacer-left"
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/about/pricing"
- >
- billing.pricing.learn_more
- </Link>
- </div>
-</CardPlan>
-`;
-
-exports[`should be actionable 1`] = `
-<div
- className="card-plan card-plan-actionable"
- onClick={[MockFunction]}
- role="radio"
- tabIndex={0}
->
- <h2
- className="card-plan-header big-spacer-bottom"
- >
- <span
- className="display-flex-center"
- >
- <i
- className="icon-radio spacer-right"
- />
- Free Plan
- </span>
- </h2>
- <div
- className="card-plan-body"
- >
- <div>
- content
- </div>
- </div>
-</div>
-`;
-
-exports[`should be actionable 2`] = `
-<div
- aria-checked={true}
- className="card-plan card-plan-actionable selected"
- onClick={
- [MockFunction] {
- "calls": Array [
- Array [
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
- ],
- ],
- "results": Array [
- Object {
- "isThrow": false,
- "value": undefined,
- },
- ],
- }
- }
- role="radio"
- tabIndex={0}
->
- <h2
- className="card-plan-header big-spacer-bottom"
- >
- <span
- className="display-flex-center"
- >
- <i
- className="icon-radio spacer-right is-checked"
- />
- Free Plan
- </span>
- <span
- className="card-plan-price"
- >
- billing.price_format.0
- </span>
- </h2>
- <div
- className="card-plan-body"
- >
- <div>
- content
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correctly 1`] = `
-<div
- className="card-plan"
- role="radio"
- tabIndex={0}
->
- <h2
- className="card-plan-header big-spacer-bottom"
- >
- <span
- className="display-flex-center"
- >
- Paid Plan
- </span>
- <FormattedMessage
- defaultMessage="billing.price_from_x"
- id="billing.price_from_x"
- values={
- Object {
- "price": <span
- className="card-plan-price"
- >
- billing.price_format.10
- </span>,
- }
- }
- />
- </h2>
- <div
- className="card-plan-body"
- >
- <div>
- content
- </div>
- </div>
- <div
- className="card-plan-recommended"
- >
- <RecommendedIcon
- className="spacer-right"
- />
- <FormattedMessage
- defaultMessage="Recommended for you"
- id="Recommended for you"
- values={
- Object {
- "recommended": <strong>
- recommended
- </strong>,
- }
- }
- />
- </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/FreeCardPlan-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/FreeCardPlan-test.tsx.snap
new file mode 100644
index 00000000000..096ad74b503
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/FreeCardPlan-test.tsx.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<RadioCard
+ title="billing.free_plan.title"
+ titleInfo="billing.price_format.0"
+>
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
+</RadioCard>
+`;
+
+exports[`should render disabled with info 1`] = `
+<RadioCard
+ disabled={true}
+ title="billing.free_plan.title"
+ titleInfo="billing.price_format.0"
+>
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
+ <Alert
+ variant="info"
+ >
+ <FormattedMessage
+ defaultMessage="billing.free_plan.not_available_info"
+ id="billing.free_plan.not_available_info"
+ values={
+ Object {
+ "alm": "GitHub",
+ }
+ }
+ />
+ </Alert>
+</RadioCard>
+`;
+
+exports[`should render with warning 1`] = `
+<RadioCard
+ selected={true}
+ title="billing.free_plan.title"
+ titleInfo="billing.price_format.0"
+>
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
+ <Alert
+ variant="warning"
+ >
+ <FormattedMessage
+ defaultMessage="billing.free_plan.private_repo_warning"
+ id="billing.free_plan.private_repo_warning"
+ values={
+ Object {
+ "alm": "GitHub",
+ }
+ }
+ />
+ </Alert>
+</RadioCard>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
index 1003db90587..8e373bfac39 100644
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
@@ -14,7 +14,8 @@ exports[`should render correctly 1`] = `
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/PaidCardPlan-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/PaidCardPlan-test.tsx.snap
new file mode 100644
index 00000000000..94ac5a89244
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/PaidCardPlan-test.tsx.snap
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<RadioCard
+ recommended="billing.paid_plan.recommended"
+ title="billing.paid_plan.title"
+ titleInfo={
+ <FormattedMessage
+ defaultMessage="billing.price_from_x"
+ id="billing.price_from_x"
+ values={
+ Object {
+ "price": <span
+ className="big"
+ >
+ billing.price_format.10
+ </span>,
+ }
+ }
+ />
+ }
+>
+ <UpgradeOrganizationAdvantages />
+ <div
+ className="big-spacer-left"
+ >
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/about/pricing"
+ >
+ billing.pricing.learn_more
+ </Link>
+ </div>
+</RadioCard>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap
index 249f04ebdce..e389049612a 100644
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap
@@ -2,9 +2,23 @@
exports[`should render correctly 1`] = `
<Fragment>
- <CardPlan
- startingPrice={10}
+ <RadioCard
title="billing.upgrade_box.header"
+ titleInfo={
+ <FormattedMessage
+ defaultMessage="billing.price_from_x"
+ id="billing.price_from_x"
+ values={
+ Object {
+ "price": <span
+ className="big"
+ >
+ billing.price_format.10
+ </span>,
+ }
+ }
+ />
+ }
>
<UpgradeOrganizationAdvantages />
<div
@@ -26,6 +40,6 @@ exports[`should render correctly 1`] = `
billing.pricing.learn_more
</Link>
</div>
- </CardPlan>
+ </RadioCard>
</Fragment>
`;
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx b/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
index 29c1a79e333..007b7b23d2b 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
export default function AlmApplicationInstalling({ almKey }: { almKey?: string }) {
return (
@@ -29,9 +30,10 @@ export default function AlmApplicationInstalling({ almKey }: { almKey?: string }
<div className="huge-spacer-top text-center">
<i className="spinner" />
<p className="big-spacer-top">
- {almKey
- ? translate('onboarding.import_organization.installing', almKey)
- : translate('onboarding.import_organization.installing')}
+ {translate(
+ 'onboarding.import_organization.installing',
+ sanitizeAlmId(almKey) || 'ALM'
+ )}
</p>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx
index a14261cacde..da1dc977e4f 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx
@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { FreeCardPlan, PaidCardPlan } from '../components/CardPlan';
+import FreeCardPlan from '../components/FreeCardPlan';
+import PaidCardPlan from '../components/PaidCardPlan';
import { translate } from '../../../helpers/l10n';
export enum Plan {
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AlmApplicationInstalling-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AlmApplicationInstalling-test.tsx
new file mode 100644
index 00000000000..60673608d36
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AlmApplicationInstalling-test.tsx
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import AlmApplicationInstalling from '../AlmApplicationInstalling';
+
+it('should render correctly', () => {
+ expect(shallow(<AlmApplicationInstalling />)).toMatchSnapshot();
+ expect(shallow(<AlmApplicationInstalling almKey="github" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
index 1537668a8fb..9330ea3039c 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
@@ -30,7 +30,11 @@ import {
import { getSubscriptionPlans } from '../../../../api/billing';
import { getOrganizations } from '../../../../api/organizations';
import { get, remove } from '../../../../helpers/storage';
-import { mockRouter } from '../../../../helpers/testMocks';
+import {
+ mockRouter,
+ mockOrganizationWithAdminActions,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
jest.mock('../../../../api/billing', () => ({
@@ -320,9 +324,9 @@ function shallowRender(props: Partial<CreateOrganization['props']> = {}) {
skipOnboarding={jest.fn()}
updateOrganization={jest.fn()}
userOrganizations={[
- { actions: { admin: true }, key: 'foo', name: 'Foo' },
- { actions: { admin: true }, alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' },
- { actions: { admin: false }, key: 'baz', name: 'Baz' }
+ mockOrganizationWithAdminActions(),
+ mockOrganizationWithAdminActions(mockOrganizationWithAlm({ key: 'bar', name: 'Bar' })),
+ mockOrganizationWithAdminActions({ key: 'baz', name: 'Baz' }, { admin: false })
]}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AlmApplicationInstalling-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AlmApplicationInstalling-test.tsx.snap
new file mode 100644
index 00000000000..a66e47ed6c5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AlmApplicationInstalling-test.tsx.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<DeferredSpinner
+ customSpinner={
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="huge-spacer-top text-center"
+ >
+ <i
+ className="spinner"
+ />
+ <p
+ className="big-spacer-top"
+ >
+ onboarding.import_organization.installing.ALM
+ </p>
+ </div>
+ </div>
+ }
+ timeout={100}
+/>
+`;
+
+exports[`should render correctly 2`] = `
+<DeferredSpinner
+ customSpinner={
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="huge-spacer-top text-center"
+ >
+ <i
+ className="spinner"
+ />
+ <p
+ className="big-spacer-top"
+ >
+ onboarding.import_organization.installing.github
+ </p>
+ </div>
+ </div>
+ }
+ timeout={100}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
index 37b3a453118..ca1c9b9201c 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
@@ -20,6 +20,10 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import AutoProjectCreate from '../AutoProjectCreate';
+import {
+ mockOrganizationWithAdminActions,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
const almApplication = {
backgroundColor: 'blue',
@@ -30,19 +34,8 @@ const almApplication = {
};
const boundOrganizations: T.Organization[] = [
- {
- actions: { admin: true },
- alm: { key: 'github', url: '' },
- key: 'foo',
- name: 'Foo',
- subscription: 'FREE'
- },
- {
- alm: { key: 'github', url: '' },
- key: 'bar',
- name: 'Bar',
- subscription: 'FREE'
- }
+ mockOrganizationWithAdminActions(mockOrganizationWithAlm({ subscription: 'FREE' })),
+ mockOrganizationWithAlm({ key: 'bar', name: 'Bar', subscription: 'FREE' })
];
it('should display the provider app install button', () => {
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarCloud-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarCloud-test.tsx
index 9e1a6e66803..5d3f92ab7d8 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarCloud-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageSonarCloud-test.tsx
@@ -21,7 +21,11 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import { CreateProjectPageSonarCloud } from '../CreateProjectPageSonarCloud';
import { getAlmAppInfo } from '../../../../api/alm-integration';
-import { mockRouter } from '../../../../helpers/testMocks';
+import {
+ mockRouter,
+ mockOrganizationWithAdminActions,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
jest.mock('../../../../api/alm-integration', () => ({
@@ -86,9 +90,15 @@ function getWrapper(props = {}) {
router={mockRouter()}
skipOnboarding={jest.fn()}
userOrganizations={[
- { actions: { provision: true }, key: 'foo', name: 'Foo' },
- { actions: { provision: true }, alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' },
- { actions: { provision: false }, key: 'baz', name: 'Baz' }
+ mockOrganizationWithAdminActions({}, { admin: false, provision: true }),
+ mockOrganizationWithAdminActions(mockOrganizationWithAlm({ key: 'bar', name: 'Bar' }), {
+ admin: false,
+ provision: true
+ }),
+ mockOrganizationWithAdminActions(
+ { key: 'baz', name: 'Baz' },
+ { admin: false, provision: false }
+ )
]}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx
index 44a1183baed..35ff3d1878e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/OrganizationInput-test.tsx
@@ -20,12 +20,13 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { OrganizationInput } from '../OrganizationInput';
-import { mockRouter } from '../../../../helpers/testMocks';
+import {
+ mockRouter,
+ mockOrganization,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
-const organizations = [
- { key: 'foo', name: 'Foo' },
- { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
-];
+const organizations = [mockOrganization(), mockOrganizationWithAlm({ key: 'bar', name: 'Bar' })];
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
index 23a3c2cf5c6..025209632ce 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
@@ -23,6 +23,10 @@ import { shallow } from 'enzyme';
import RemoteRepositories from '../RemoteRepositories';
import { getRepositories } from '../../../../api/alm-integration';
import { waitAndUpdate } from '../../../../helpers/testUtils';
+import {
+ mockOrganizationWithAlm,
+ mockOrganizationWithAdminActions
+} from '../../../../helpers/testMocks';
jest.mock('../../../../api/alm-integration', () => ({
getRepositories: jest.fn().mockResolvedValue({
@@ -46,12 +50,7 @@ const almApplication = {
name: 'GitHub'
};
-const organization: T.Organization = {
- alm: { key: 'github', url: '' },
- key: 'sonarsource',
- name: 'SonarSource',
- subscription: 'FREE'
-};
+const organization: T.Organization = mockOrganizationWithAlm({ subscription: 'FREE' });
beforeEach(() => {
(getRepositories as jest.Mock<any>).mockClear();
@@ -62,7 +61,7 @@ it('should display the list of repositories', async () => {
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
- expect(getRepositories).toHaveBeenCalledWith({ organization: 'sonarsource' });
+ expect(getRepositories).toHaveBeenCalledWith({ organization: 'foo' });
});
it('should display the organization upgrade box', async () => {
@@ -81,13 +80,9 @@ it('should not display the organization upgrade box', () => {
repositories: [{ label: 'Bar Project', installationKey: 'github/bar', private: true }]
});
const wrapper = shallowRender({
- organization: {
- actions: { admin: true },
- alm: { key: 'github', url: '' },
- key: 'foobar',
- name: 'FooBar',
- subscription: 'PAID'
- }
+ organization: mockOrganizationWithAdminActions(
+ mockOrganizationWithAlm({ subscription: 'PAID' })
+ )
});
expect(wrapper.find('UpgradeOrganizationBox').exists()).toBe(false);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx
index 86339a2d849..a7c3844ded7 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx
@@ -22,6 +22,7 @@ import { shallow } from 'enzyme';
import SetupProjectBox from '../SetupProjectBox';
import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
import { provisionProject } from '../../../../api/alm-integration';
+import { mockOrganizationWithAlm } from '../../../../helpers/testMocks';
jest.mock('../../../../api/alm-integration', () => ({
provisionProject: jest
@@ -29,18 +30,6 @@ jest.mock('../../../../api/alm-integration', () => ({
.mockResolvedValue({ projects: [{ projectKey: 'awesome' }, { projectKey: 'foo' }] })
}));
-const organization: T.Organization = {
- alm: { key: 'github', url: '' },
- key: 'sonarsource',
- name: 'SonarSource',
- subscription: 'FREE'
-};
-
-const selectedRepositories = [
- { label: 'Awesome Project', installationKey: 'github/awesome' },
- { label: 'Foo', installationKey: 'github/foo', private: true }
-];
-
it('should correctly create projects', async () => {
const onProjectCreate = jest.fn();
const wrapper = shallowRender({ onProjectCreate });
@@ -49,11 +38,11 @@ it('should correctly create projects', async () => {
submit(wrapper.find('form'));
expect(provisionProject).toBeCalledWith({
installationKeys: ['github/awesome', 'github/foo'],
- organization: 'sonarsource'
+ organization: 'foo'
});
await waitAndUpdate(wrapper);
- expect(onProjectCreate).toBeCalledWith(['awesome', 'foo'], 'sonarsource');
+ expect(onProjectCreate).toBeCalledWith(['awesome', 'foo'], 'foo');
});
function shallowRender(props: Partial<SetupProjectBox['props']> = {}) {
@@ -61,8 +50,11 @@ function shallowRender(props: Partial<SetupProjectBox['props']> = {}) {
<SetupProjectBox
onProjectCreate={jest.fn()}
onProvisionFail={jest.fn()}
- organization={organization}
- selectedRepositories={selectedRepositories}
+ organization={mockOrganizationWithAlm({ subscription: 'FREE' })}
+ selectedRepositories={[
+ { label: 'Awesome Project', installationKey: 'github/awesome' },
+ { label: 'Foo', installationKey: 'github/foo', private: true }
+ ]}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
index 0f3247f2994..61c5ce222bb 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
@@ -14,7 +14,8 @@ exports[`should display the bound organizations dropdown with the remote reposit
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "foo",
"name": "Foo",
@@ -23,7 +24,8 @@ exports[`should display the bound organizations dropdown with the remote reposit
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
@@ -51,7 +53,8 @@ exports[`should display the bound organizations dropdown with the remote reposit
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "foo",
"name": "Foo",
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarCloud-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarCloud-test.tsx.snap
index cbec17a9d1c..c0407c7e630 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarCloud-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageSonarCloud-test.tsx.snap
@@ -81,11 +81,13 @@ exports[`should render correctly 2`] = `
Array [
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
@@ -136,6 +138,7 @@ exports[`should render with Manual creation only 1`] = `
Array [
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"key": "foo",
@@ -143,11 +146,13 @@ exports[`should render with Manual creation only 1`] = `
},
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
@@ -211,11 +216,13 @@ exports[`should switch tabs 1`] = `
Array [
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap
index ba9fc2ff450..a655ac75dbe 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap
@@ -33,7 +33,8 @@ exports[`should render correctly 1`] = `
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
index 60e50364c99..ed810b7ac31 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
@@ -33,10 +33,11 @@ exports[`should display the list of repositories 1`] = `
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
- "key": "sonarsource",
- "name": "SonarSource",
+ "key": "foo",
+ "name": "Foo",
"subscription": "FREE",
}
}
@@ -121,10 +122,11 @@ exports[`should display the list of repositories 2`] = `
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
- "key": "sonarsource",
- "name": "SonarSource",
+ "key": "foo",
+ "name": "Foo",
"subscription": "FREE",
}
}
@@ -146,10 +148,11 @@ exports[`should display the organization upgrade box 1`] = `
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
- "key": "sonarsource",
- "name": "SonarSource",
+ "key": "foo",
+ "name": "Foo",
"subscription": "FREE",
}
}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
index 938e5a47d05..ba7cabf81a0 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
@@ -18,16 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import HelpTooltip from '../../components/controls/HelpTooltip';
import SearchBox from '../../components/controls/SearchBox';
+import { getAlmMembersUrl, sanitizeAlmId } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
-import { translate } from '../../helpers/l10n';
-interface Props {
+export interface Props {
+ currentUser: T.LoggedInUser;
handleSearch: (query?: string) => void;
+ organization: T.Organization;
total?: number;
}
-export default function MembersListHeader({ handleSearch, total }: Props) {
+export default function MembersListHeader({
+ currentUser,
+ handleSearch,
+ organization,
+ total
+}: Props) {
return (
<div className="panel panel-vertical bordered-bottom spacer-bottom">
<SearchBox
@@ -38,6 +47,38 @@ export default function MembersListHeader({ handleSearch, total }: Props) {
{total !== undefined && (
<span className="pull-right little-spacer-top">
<strong>{formatMeasure(total, 'INT')}</strong> {translate('organization.members.members')}
+ {organization.alm &&
+ organization.alm.membersSync && (
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div className="abs-width-300 markdown cut-margins">
+ <p>
+ {translate(
+ 'organization.members.auto_sync_total_help',
+ sanitizeAlmId(organization.alm.key) || ''
+ )}
+ </p>
+ {currentUser.personalOrganization !== organization.key && (
+ <>
+ <hr />
+ <p>
+ <a
+ href={getAlmMembersUrl(organization.alm)}
+ rel="noopener noreferrer"
+ target="_blank">
+ {translateWithParameters(
+ 'organization.members.see_all_members_on_x',
+ translate(sanitizeAlmId(organization.alm.key) || '')
+ )}
+ </a>
+ </p>
+ </>
+ )}
+ </div>
+ }
+ />
+ )}
</span>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
index ec540fed313..82c68eba3d6 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
@@ -18,35 +18,110 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
-import { translate } from '../../helpers/l10n';
+import AddMemberForm from './AddMemberForm';
+import SyncMemberForm from './SyncMemberForm';
import DeferredSpinner from '../../components/common/DeferredSpinner';
+import DocTooltip from '../../components/docs/DocTooltip';
+import NewInfoBox from '../../components/ui/NewInfoBox';
+import { sanitizeAlmId } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { getCurrentUserSetting, Store } from '../../store/rootReducer';
+import { setCurrentUserSetting } from '../../store/users';
interface Props {
- children?: React.ReactNode;
+ dismissSyncNotifOrg: string[];
+ handleAddMember: (member: T.OrganizationMember) => void;
loading: boolean;
+ members?: T.OrganizationMember[];
+ organization: T.Organization;
+ setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
}
-export default function MembersPageHeader(props: Props) {
- return (
- <header className="page-header">
- <h1 className="page-title">{translate('organization.members.page')}</h1>
- <DeferredSpinner loading={props.loading} />
- {props.children}
- <p className="page-description">
- <FormattedMessage
- defaultMessage={translate('organization.members.page.description')}
- id="organization.members.page.description"
- values={{
- link: (
- <Link to="/documentation/organizations/manage-team/">
- {translate('organization.members.manage_a_team')}
- </Link>
- )
- }}
- />
- </p>
- </header>
- );
+export class MembersPageHeader extends React.PureComponent<Props> {
+ handleDismissSyncNotif = () => {
+ const { dismissSyncNotifOrg, organization } = this.props;
+ this.props.setCurrentUserSetting({
+ key: 'organizations.members.dismissSyncNotif',
+ value: [...dismissSyncNotifOrg, organization.key].join(',')
+ });
+ };
+
+ render() {
+ const { dismissSyncNotifOrg, members, organization } = this.props;
+ const memberLogins = members ? members.map(member => member.login) : [];
+ const isAdmin = organization.actions && organization.actions.admin;
+ const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
+ const hasMemberSync = organization.alm && organization.alm.membersSync;
+ const showSyncNotif =
+ isAdmin &&
+ organization.alm &&
+ !hasMemberSync &&
+ !dismissSyncNotifOrg.some(orgKey => orgKey === organization.key);
+
+ return (
+ <header className="page-header">
+ <h1 className="page-title">{translate('organization.members.page')}</h1>
+ <DeferredSpinner loading={this.props.loading} />
+ {isAdmin && (
+ <div className="page-actions text-right">
+ {almKey && !showSyncNotif && <SyncMemberForm organization={organization} />}
+ {!hasMemberSync && (
+ <div className="display-inline-block spacer-left spacer-bottom">
+ <AddMemberForm
+ addMember={this.props.handleAddMember}
+ memberLogins={memberLogins}
+ organization={organization}
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
+ />
+ </div>
+ )}
+ {almKey &&
+ showSyncNotif && (
+ <NewInfoBox
+ description={translate('organization.members.auto_sync_members_from_org', almKey)}
+ onClose={this.handleDismissSyncNotif}
+ title={translateWithParameters(
+ 'organization.members.auto_sync_with_x',
+ translate(almKey)
+ )}>
+ <SyncMemberForm organization={organization} />
+ </NewInfoBox>
+ )}
+ </div>
+ )}
+ <div className="page-description">
+ <FormattedMessage
+ defaultMessage={translate('organization.members.page.description')}
+ id="organization.members.page.description"
+ values={{
+ link: (
+ <Link to="/documentation/organizations/manage-team/">
+ {translate('organization.members.manage_a_team')}
+ </Link>
+ )
+ }}
+ />
+ </div>
+ </header>
+ );
+ }
}
+
+const mapStateToProps = (state: Store) => ({
+ dismissSyncNotifOrg: (
+ getCurrentUserSetting(state, 'organizations.members.dismissSyncNotif') || ''
+ ).split(',')
+});
+
+const mapDispatchToProps = { setCurrentUserSetting };
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MembersPageHeader);
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
index 215415f6216..46e1162372b 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
@@ -22,15 +22,14 @@ import Helmet from 'react-helmet';
import MembersPageHeader from './MembersPageHeader';
import MembersListHeader from './MembersListHeader';
import MembersList from './MembersList';
-import AddMemberForm from './AddMemberForm';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import ListFooter from '../../components/controls/ListFooter';
-import DocTooltip from '../../components/docs/DocTooltip';
import { translate } from '../../helpers/l10n';
import { searchMembers, addMember, removeMember } from '../../api/organizations';
import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../api/user_groups';
interface Props {
+ currentUser: T.LoggedInUser;
organization: T.Organization;
}
@@ -187,31 +186,25 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
render() {
const { organization } = this.props;
const { groups, loading, members, paging } = this.state;
- const memberLogins = members ? members.map(member => member.login) : [];
return (
<div className="page page-limited">
<Helmet title={translate('organization.members.page')} />
<Suggestions suggestions="organization_members" />
- <MembersPageHeader loading={loading}>
- {organization.actions &&
- organization.actions.admin && (
- <div className="page-actions">
- <AddMemberForm
- addMember={this.handleAddMember}
- memberLogins={memberLogins}
- organization={organization}
- />
- <DocTooltip
- className="spacer-left"
- doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
- />
- </div>
- )}
- </MembersPageHeader>
+ <MembersPageHeader
+ handleAddMember={this.handleAddMember}
+ loading={loading}
+ members={members}
+ organization={organization}
+ />
{members !== undefined &&
paging !== undefined && (
<>
- <MembersListHeader handleSearch={this.handleSearchMembers} total={paging.total} />
+ <MembersListHeader
+ currentUser={this.props.currentUser}
+ handleSearch={this.handleSearchMembers}
+ organization={organization}
+ total={paging.total}
+ />
<MembersList
members={members}
organization={organization}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
index 0390d301ea3..05c709f3920 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
@@ -20,6 +20,7 @@
import { connect } from 'react-redux';
import OrganizationMembers from './OrganizationMembers';
import { getOrganizationByKey, Store } from '../../store/rootReducer';
+import { withCurrentUser } from '../../components/hoc/withCurrentUser';
interface OwnProps {
params: { organizationKey: string };
@@ -33,4 +34,4 @@ const mapStateToProps = (state: Store, ownProps: OwnProps): StateProps => {
return { organization: getOrganizationByKey(state, ownProps.params.organizationKey) };
};
-export default connect(mapStateToProps)(OrganizationMembers);
+export default withCurrentUser(connect(mapStateToProps)(OrganizationMembers));
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
new file mode 100644
index 00000000000..b04c679563f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
@@ -0,0 +1,169 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { Link } from 'react-router';
+import ConfirmButton from '../../components/controls/ConfirmButton';
+import RadioCard from '../../components/controls/RadioCard';
+import { Alert } from '../../components/ui/Alert';
+import { Button } from '../../components/ui/buttons';
+import { setOrganizationMemberSync, syncMembers } from '../../api/organizations';
+import { sanitizeAlmId, isGithub } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { fetchOrganization } from '../../store/rootActions';
+
+interface Props {
+ fetchOrganization: (key: string) => void;
+ organization: T.Organization;
+}
+
+interface State {
+ membersSync: boolean;
+}
+
+export class SyncMemberForm extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ membersSync: Boolean(props.organization.alm && props.organization.alm.membersSync)
+ };
+ }
+
+ handleConfirm = () => {
+ const { organization } = this.props;
+ const { membersSync } = this.state;
+ return setOrganizationMemberSync({
+ organization: organization.key,
+ enabled: membersSync
+ }).then(() => {
+ this.props.fetchOrganization(organization.key);
+ if (membersSync && isGithub(organization.alm && organization.alm.key)) {
+ return syncMembers(organization.key);
+ }
+ return Promise.resolve();
+ });
+ };
+
+ handleManualClick = () => {
+ this.setState({ membersSync: false });
+ };
+
+ handleAutoClick = () => {
+ this.setState({ membersSync: true });
+ };
+
+ renderModalBody = () => {
+ const { membersSync } = this.state;
+ const { organization } = this.props;
+ const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
+ return (
+ <>
+ {translate('organization.members.management.description')}
+ <Link
+ className="spacer-left"
+ target="_blank"
+ to={{ pathname: '/documentation/organizations/manage-team/' }}>
+ {translate('learn_more')}
+ </Link>
+ <div className="display-flex-stretch big-spacer-top">
+ <RadioCard
+ onClick={this.handleManualClick}
+ selected={!membersSync}
+ title={translate('organization.members.management.manual')}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ <li className="spacer-bottom">
+ {translate('organization.members.management.manual.add_members_manually')}
+ </li>
+ <li>
+ {translate('organization.members.management.manual.choose_members_permissions')}
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={this.handleAutoClick}
+ selected={membersSync}
+ title={translateWithParameters(
+ 'organization.members.management.automatic',
+ translate(almKey || '')
+ )}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ {almKey && (
+ <>
+ <li className="spacer-bottom">
+ {translate(
+ 'organization.members.management.automatic.synchronized_from',
+ almKey
+ )}
+ </li>
+ <li className="spacer-bottom">
+ {translate(
+ 'organization.members.management.automatic.members_changes_reflected',
+ almKey
+ )}
+ </li>
+ </>
+ )}
+ <li>
+ {translate(
+ 'organization.members.management.automatic.still_choose_members_permissions'
+ )}
+ </li>
+ </ul>
+ </div>
+ {(!organization.alm || !organization.alm.membersSync) && (
+ <Alert className="big-spacer-top" variant="warning">
+ {translate('organization.members.management.automatic.warning')}
+ </Alert>
+ )}
+ </RadioCard>
+ </div>
+ </>
+ );
+ };
+
+ render() {
+ const { organization } = this.props;
+ const orgMemberSync = Boolean(organization.alm && organization.alm.membersSync);
+ return (
+ <ConfirmButton
+ cancelButtonText={translate('close')}
+ confirmButtonText={translate('save')}
+ confirmDisable={this.state.membersSync === orgMemberSync}
+ medium={true}
+ modalBody={this.renderModalBody()}
+ modalHeader={translate('organization.members.management.title')}
+ onConfirm={this.handleConfirm}>
+ {({ onClick }) => (
+ <Button onClick={onClick}>{translate('organization.members.config_synchro')}</Button>
+ )}
+ </ConfirmButton>
+ );
+ }
+}
+
+const mapDispatchToProps = { fetchOrganization };
+
+export default connect(
+ null,
+ mapDispatchToProps
+)(SyncMemberForm);
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
index 3105927acfa..7c97806d07e 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
@@ -19,14 +19,54 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import MembersListHeader from '../MembersListHeader';
+import MembersListHeader, { Props } from '../MembersListHeader';
+import {
+ mockOrganization,
+ mockCurrentUser,
+ mockOrganizationWithAlm
+} from '../../../helpers/testMocks';
it('should render without the total', () => {
- const wrapper = shallow(<MembersListHeader handleSearch={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender({ total: undefined })).toMatchSnapshot();
});
it('should render with the total', () => {
- const wrapper = shallow(<MembersListHeader handleSearch={jest.fn()} total={8} />);
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
+
+it('should render a help tooltip', () => {
+ expect(
+ shallowRender({ organization: mockOrganizationWithAlm({}, { membersSync: true }) }).find(
+ 'HelpTooltip'
+ )
+ ).toMatchSnapshot();
+ expect(
+ shallowRender({
+ organization: mockOrganizationWithAlm(
+ {},
+ { key: 'bitbucket', membersSync: true, url: 'https://bitbucket.com/foo' }
+ )
+ }).find('HelpTooltip')
+ ).toMatchSnapshot();
+});
+
+it('should not render link in help tooltip', () => {
+ expect(
+ shallowRender({
+ currentUser: mockCurrentUser({ personalOrganization: 'foo' }),
+ organization: mockOrganizationWithAlm({}, { membersSync: true })
+ }).find('HelpTooltip')
+ ).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <MembersListHeader
+ currentUser={mockCurrentUser()}
+ handleSearch={jest.fn()}
+ organization={mockOrganization()}
+ total={8}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
index 538d093be88..7f74a8eff3c 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
@@ -19,13 +19,52 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import MembersPageHeader from '../MembersPageHeader';
+import { MembersPageHeader } from '../MembersPageHeader';
+import {
+ mockOrganization,
+ mockOrganizationWithAlm,
+ mockOrganizationWithAdminActions
+} from '../../../helpers/testMocks';
-it('should render', () => {
- const wrapper = shallow(
- <MembersPageHeader loading={true}>
- <span>children test</span>
- </MembersPageHeader>
- );
- expect(wrapper).toMatchSnapshot();
+it('should render correctly', () => {
+ expect(shallowRender({ loading: true })).toMatchSnapshot();
+});
+
+it('should render for admin', () => {
+ expect(
+ shallowRender({ organization: mockOrganization({ actions: { admin: true } }) })
+ ).toMatchSnapshot();
+});
+
+it('should render for bound organization without sync', () => {
+ const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions());
+ expect(shallowRender({ organization })).toMatchSnapshot();
+
+ const wrapper = shallowRender({ organization, dismissSyncNotifOrg: [organization.key] });
+ expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
+ expect(wrapper.find('NewInfoBox').exists()).toBe(false);
+});
+
+it('should render for bound organization with sync', () => {
+ const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions(), {
+ membersSync: true
+ });
+ const wrapper = shallowRender({ organization });
+ expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
+ expect(wrapper.find('AddMemberForm').exists()).toBe(false);
+ expect(wrapper.find('NewInfoBox').exists()).toBe(false);
});
+
+function shallowRender(props: Partial<MembersPageHeader['props']> = {}) {
+ return shallow(
+ <MembersPageHeader
+ dismissSyncNotifOrg={[]}
+ handleAddMember={jest.fn()}
+ loading={false}
+ members={[]}
+ organization={mockOrganization()}
+ setCurrentUserSetting={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
index 43c07605766..7861581aa2e 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
@@ -20,9 +20,14 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationMembers from '../OrganizationMembers';
-import { waitAndUpdate } from '../../../helpers/testUtils';
import { searchMembers, addMember, removeMember } from '../../../api/organizations';
import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
+import {
+ mockOrganization,
+ mockCurrentUser,
+ mockOrganizationWithAdminActions
+} from '../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../helpers/testUtils';
jest.mock('../../../api/organizations', () => ({
addMember: jest.fn().mockResolvedValue({ login: 'bar', name: 'Bar', groupCount: 1 }),
@@ -48,15 +53,13 @@ jest.mock('../../../api/user_groups', () => ({
})
}));
-const organization = { key: 'foo', name: 'Foo' };
-
beforeEach(() => {
(searchMembers as jest.Mock).mockClear();
(searchUsersGroups as jest.Mock).mockClear();
});
it('should fetch members and render for non-admin', async () => {
- const wrapper = shallow(<OrganizationMembers organization={organization} />);
+ const wrapper = shallowRender({ organization: mockOrganization() });
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
@@ -64,40 +67,31 @@ it('should fetch members and render for non-admin', async () => {
expect(searchMembers).toBeCalledWith({ organization: 'foo', ps: 50, q: undefined });
});
-it('should fetch members and groups and render for admin', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+it('should fetch members and groups for admin', async () => {
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
expect(searchMembers).toBeCalledWith({ organization: 'foo', ps: 50, q: undefined });
expect(searchUsersGroups).toBeCalledWith({ organization: 'foo' });
});
it('should search users', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersListHeader').prop<Function>('handleSearch')('user');
expect(searchMembers).lastCalledWith({ organization: 'foo', ps: 50, q: 'user' });
});
it('should load more members', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('ListFooter').prop<Function>('loadMore')();
expect(searchMembers).lastCalledWith({ organization: 'foo', p: 2, ps: 50, q: undefined });
});
it('should add new member', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
- wrapper.find('AddMemberForm').prop<Function>('addMember')({ login: 'bar' });
+ wrapper.find('Connect(MembersPageHeader)').prop<Function>('handleAddMember')({ login: 'bar' });
await waitAndUpdate(wrapper);
expect(
wrapper
@@ -110,9 +104,7 @@ it('should add new member', async () => {
});
it('should remove member', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersList').prop<Function>('removeMember')({ login: 'john' });
await waitAndUpdate(wrapper);
@@ -127,9 +119,7 @@ it('should remove member', async () => {
});
it('should update groups', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersList').prop<Function>('updateMemberGroups')(
{ login: 'john' },
@@ -148,3 +138,13 @@ it('should update groups', async () => {
expect(removeUserFromGroup).toHaveBeenCalledTimes(1);
expect(removeUserFromGroup).toBeCalledWith({ login: 'john', name: 'birds', organization: 'foo' });
});
+
+function shallowRender(props: Partial<OrganizationMembers['props']> = {}) {
+ return shallow(
+ <OrganizationMembers
+ currentUser={mockCurrentUser()}
+ organization={mockOrganizationWithAdminActions()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx
new file mode 100644
index 00000000000..4410d45c301
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { SyncMemberForm } from '../SyncMemberForm';
+import { setOrganizationMemberSync, syncMembers } from '../../../api/organizations';
+import { mockOrganizationWithAlm } from '../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+
+jest.mock('../../../api/organizations', () => ({
+ setOrganizationMemberSync: jest.fn().mockResolvedValue(undefined),
+ syncMembers: jest.fn().mockResolvedValue(undefined)
+}));
+
+beforeEach(() => {
+ (setOrganizationMemberSync as jest.Mock).mockClear();
+ (syncMembers as jest.Mock).mockClear();
+});
+
+it('should allow to switch to automatic mode with github', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({ fetchOrganization });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: true });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: true });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).toHaveBeenCalledWith('foo');
+});
+
+it('should allow to switch to automatic mode with bitbucket', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({
+ fetchOrganization,
+ organization: mockOrganizationWithAlm({}, { key: 'bitbucket' })
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: true });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: true });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).not.toHaveBeenCalled();
+});
+
+it('should allow to switch to manual mode', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({
+ fetchOrganization,
+ organization: mockOrganizationWithAlm({}, { membersSync: true })
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: false });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: false });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).not.toHaveBeenCalled();
+});
+
+function shallowRender(props: Partial<SyncMemberForm['props']> = {}) {
+ return shallow<SyncMemberForm>(
+ <SyncMemberForm
+ fetchOrganization={jest.fn()}
+ organization={mockOrganizationWithAlm()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
index 7bfe2725e60..1d8ecfb92df 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
@@ -1,5 +1,74 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should not render link in help tooltip 1`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.github
+ </p>
+ </div>
+ }
+/>
+`;
+
+exports[`should render a help tooltip 1`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.github
+ </p>
+ <React.Fragment>
+ <hr />
+ <p>
+ <a
+ href="https://github.com/orgs/foo/people"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ organization.members.see_all_members_on_x.github
+ </a>
+ </p>
+ </React.Fragment>
+ </div>
+ }
+/>
+`;
+
+exports[`should render a help tooltip 2`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.bitbucket
+ </p>
+ <React.Fragment>
+ <hr />
+ <p>
+ <a
+ href="https://bitbucket.com/foo/profile/members"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ organization.members.see_all_members_on_x.bitbucket
+ </a>
+ </p>
+ </React.Fragment>
+ </div>
+ }
+/>
+`;
+
exports[`should render with the total 1`] = `
<div
className="panel panel-vertical bordered-bottom spacer-bottom"
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
index 95179079487..b0bc7e4a009 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render 1`] = `
+exports[`should render correctly 1`] = `
<header
className="page-header"
>
@@ -13,10 +13,7 @@ exports[`should render 1`] = `
loading={true}
timeout={100}
/>
- <span>
- children test
- </span>
- <p
+ <div
className="page-description"
>
<FormattedMessage
@@ -34,6 +31,153 @@ exports[`should render 1`] = `
}
}
/>
- </p>
+ </div>
+</header>
+`;
+
+exports[`should render for admin 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ organization.members.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions text-right"
+ >
+ <div
+ className="display-inline-block spacer-left spacer-bottom"
+ >
+ <AddMemberForm
+ addMember={[MockFunction]}
+ memberLogins={Array []}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </div>
+ </div>
+ <div
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="organization.members.page.description"
+ id="organization.members.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/organizations/manage-team/"
+ >
+ organization.members.manage_a_team
+ </Link>,
+ }
+ }
+ />
+ </div>
+</header>
+`;
+
+exports[`should render for bound organization without sync 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ organization.members.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions text-right"
+ >
+ <div
+ className="display-inline-block spacer-left spacer-bottom"
+ >
+ <AddMemberForm
+ addMember={[MockFunction]}
+ memberLogins={Array []}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "membersSync": false,
+ "url": "https://github.com/foo",
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </div>
+ <NewInfoBox
+ description="organization.members.auto_sync_members_from_org.github"
+ onClose={[Function]}
+ title="organization.members.auto_sync_with_x.github"
+ >
+ <Connect(SyncMemberForm)
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "membersSync": false,
+ "url": "https://github.com/foo",
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ </NewInfoBox>
+ </div>
+ <div
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="organization.members.page.description"
+ id="organization.members.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/organizations/manage-team/"
+ >
+ organization.members.manage_a_team
+ </Link>,
+ }
+ }
+ />
+ </div>
</header>
`;
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
index 90b44168efc..dd26a5928f4 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should fetch members and groups and render for admin 1`] = `
+exports[`should fetch members and render for non-admin 1`] = `
<div
className="page page-limited"
>
@@ -12,110 +12,15 @@ exports[`should fetch members and groups and render for admin 1`] = `
<Suggestions
suggestions="organization_members"
/>
- <MembersPageHeader
- loading={false}
- >
- <div
- className="page-actions"
- >
- <AddMemberForm
- addMember={[Function]}
- memberLogins={
- Array [
- "admin",
- "john",
- ]
- }
- organization={
- Object {
- "actions": Object {
- "admin": true,
- },
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <DocTooltip
- className="spacer-left"
- doc={Promise {}}
- />
- </div>
- </MembersPageHeader>
- <MembersListHeader
- handleSearch={[Function]}
- total={3}
- />
- <MembersList
- members={
- Array [
- Object {
- "avatar": "",
- "groupCount": 3,
- "login": "admin",
- "name": "Admin Istrator",
- },
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "groupCount": 1,
- "login": "john",
- "name": "John Doe",
- },
- ]
- }
+ <Connect(MembersPageHeader)
+ handleAddMember={[Function]}
+ loading={true}
organization={
Object {
- "actions": Object {
- "admin": true,
- },
"key": "foo",
"name": "Foo",
}
}
- organizationGroups={
- Array [
- Object {
- "default": true,
- "description": "",
- "id": 1,
- "membersCount": 2,
- "name": "Members",
- },
- Object {
- "default": false,
- "description": "",
- "id": 2,
- "membersCount": 0,
- "name": "Watchers",
- },
- ]
- }
- removeMember={[Function]}
- updateMemberGroups={[Function]}
- />
- <ListFooter
- count={2}
- loadMore={[Function]}
- ready={true}
- total={3}
- />
-</div>
-`;
-
-exports[`should fetch members and render for non-admin 1`] = `
-<div
- className="page page-limited"
->
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="organization.members.page"
- />
- <Suggestions
- suggestions="organization_members"
- />
- <MembersPageHeader
- loading={true}
/>
</div>
`;
@@ -132,11 +37,49 @@ exports[`should fetch members and render for non-admin 2`] = `
<Suggestions
suggestions="organization_members"
/>
- <MembersPageHeader
+ <Connect(MembersPageHeader)
+ handleAddMember={[Function]}
loading={false}
+ members={
+ Array [
+ Object {
+ "avatar": "",
+ "groupCount": 3,
+ "login": "admin",
+ "name": "Admin Istrator",
+ },
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "groupCount": 1,
+ "login": "john",
+ "name": "John Doe",
+ },
+ ]
+ }
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
/>
<MembersListHeader
+ currentUser={
+ Object {
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "luke",
+ "name": "Skywalker",
+ "scmAccounts": Array [],
+ }
+ }
handleSearch={[Function]}
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
total={3}
/>
<MembersList
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap
new file mode 100644
index 00000000000..b8744db2ef6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap
@@ -0,0 +1,271 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should allow to switch to automatic mode with bitbucket 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.automatic.bitbucket"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.bitbucket
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.bitbucket
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ <Alert
+ className="big-spacer-top"
+ variant="warning"
+ >
+ organization.members.management.automatic.warning
+ </Alert>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
+
+exports[`should allow to switch to automatic mode with github 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.automatic.github"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.github
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.github
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ <Alert
+ className="big-spacer-top"
+ variant="warning"
+ >
+ organization.members.management.automatic.warning
+ </Alert>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
+
+exports[`should allow to switch to manual mode 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.automatic.github"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.github
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.github
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
index 7df40517995..39519ffd939 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationNavigationHeader from '../OrganizationNavigationHeader';
+import { mockOrganizationWithAlm } from '../../../../helpers/testMocks';
it('renders', () => {
expect(
@@ -36,12 +37,7 @@ it('renders with alm integration', () => {
expect(
shallow(
<OrganizationNavigationHeader
- organization={{
- alm: { key: 'github', url: 'https://github.com/foo' },
- key: 'foo',
- name: 'Foo',
- projectVisibility: 'public'
- }}
+ organization={mockOrganizationWithAlm({ projectVisibility: 'public' })}
organizations={[]}
/>
)
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
index 5ef064c39fc..c02c0831a7f 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
@@ -76,6 +76,7 @@ exports[`renders with alm integration 1`] = `
Object {
"alm": Object {
"key": "github",
+ "membersSync": false,
"url": "https://github.com/foo",
},
"key": "foo",
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
index c3be977b72d..0718757ff1a 100644
--- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
@@ -18,40 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import ConfirmModal, { ConfirmModalProps } from './ConfirmModal';
import ModalButton, { ChildrenProps, ModalProps } from './ModalButton';
-import ConfirmModal from './ConfirmModal';
-export { ChildrenProps } from './ModalButton';
-
-interface Props {
+interface Props<T> extends ConfirmModalProps<T> {
children: (props: ChildrenProps) => React.ReactNode;
- cancelButtonText?: string;
- confirmButtonText: string;
- confirmData?: string;
- confirmDisable?: boolean;
- isDestructive?: boolean;
modalBody: React.ReactNode;
modalHeader: string;
- onConfirm: (data?: string) => void | Promise<void>;
}
interface State {
modal: boolean;
}
-export default class ConfirmButton extends React.PureComponent<Props, State> {
+export default class ConfirmButton<T> extends React.PureComponent<Props<T>, State> {
renderConfirmModal = ({ onClose }: ModalProps) => {
+ const { children, modalBody, modalHeader, ...confirmModalProps } = this.props;
return (
- <ConfirmModal
- cancelButtonText={this.props.cancelButtonText}
- confirmButtonText={this.props.confirmButtonText}
- confirmData={this.props.confirmData}
- confirmDisable={this.props.confirmDisable}
- header={this.props.modalHeader}
- isDestructive={this.props.isDestructive}
- onClose={onClose}
- onConfirm={this.props.onConfirm}>
- {this.props.modalBody}
+ <ConfirmModal header={modalHeader} onClose={onClose} {...confirmModalProps}>
+ {modalBody}
</ConfirmModal>
);
};
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
index 8ac88d222ac..f51f9704aec 100644
--- a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
@@ -18,21 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { ModalProps } from './Modal';
import SimpleModal, { ChildrenProps } from './SimpleModal';
import DeferredSpinner from '../common/DeferredSpinner';
-import { translate } from '../../helpers/l10n';
import { SubmitButton, ResetButtonLink } from '../ui/buttons';
+import { translate } from '../../helpers/l10n';
-interface Props<T> {
- children: React.ReactNode;
+export interface ConfirmModalProps<T> extends ModalProps {
cancelButtonText?: string;
confirmButtonText: string;
confirmData?: T;
confirmDisable?: boolean;
- header: string;
isDestructive?: boolean;
+ onConfirm: (data?: T) => void | Promise<void | Response>;
+}
+
+interface Props<T> extends ConfirmModalProps<T> {
+ header: string;
onClose: () => void;
- onConfirm: (data?: T) => void | Promise<void>;
}
export default class ConfirmModal<T = string> extends React.PureComponent<Props<T>> {
@@ -82,9 +85,10 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props<
};
render() {
- const { header } = this.props;
+ const { header, onClose, medium, noBackdrop, large, simple } = this.props;
+ const modalProps = { header, onClose, medium, noBackdrop, large, simple };
return (
- <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
+ <SimpleModal onSubmit={this.handleSubmit} {...modalProps}>
{this.renderModalContent}
</SimpleModal>
);
diff --git a/server/sonar-web/src/main/js/components/controls/Modal.tsx b/server/sonar-web/src/main/js/components/controls/Modal.tsx
index 7f91bb4e082..e7b14c3a1df 100644
--- a/server/sonar-web/src/main/js/components/controls/Modal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Modal.tsx
@@ -23,7 +23,8 @@ import * as classNames from 'classnames';
ReactModal.setAppElement('#content');
-interface OwnProps {
+export interface ModalProps {
+ children: React.ReactNode;
medium?: boolean;
noBackdrop?: boolean;
large?: boolean;
@@ -32,7 +33,7 @@ interface OwnProps {
type MandatoryProps = Pick<ReactModal.Props, 'contentLabel'>;
-type Props = Partial<ReactModal.Props> & MandatoryProps & OwnProps;
+type Props = Partial<ReactModal.Props> & MandatoryProps & ModalProps;
export default function Modal(props: Props) {
return (
diff --git a/server/sonar-web/src/main/js/apps/create/components/CardPlan.css b/server/sonar-web/src/main/js/components/controls/RadioCard.css
index 174e3c33d00..a5099a9a239 100644
--- a/server/sonar-web/src/main/js/apps/create/components/CardPlan.css
+++ b/server/sonar-web/src/main/js/components/controls/RadioCard.css
@@ -17,11 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-.card-plan {
+.radio-card {
display: flex;
flex-direction: column;
width: 450px;
- height: 210px;
+ min-height: 210px;
background-color: #fff;
border: solid 1px var(--barBorderColor);
border-radius: 3px;
@@ -30,70 +30,66 @@
transition: all 0.2s ease;
}
-.card-plan.animated {
+.radio-card.animated {
height: 0;
border-width: 0;
overflow: hidden;
}
-.card-plan.animated.open {
+.radio-card.animated.open {
height: 210px;
border-width: 1px;
}
-.card-plan.highlight {
+.radio-card.highlight {
box-shadow: var(--defaultShadow);
}
-.card-plan:last-child {
+.radio-card:last-child {
margin-right: 0;
}
-.card-plan:focus {
+.radio-card:focus {
outline: none;
}
-.card-plan-actionable {
+.radio-card-actionable {
cursor: pointer;
}
-.card-plan-actionable:not(.disabled):hover {
+.radio-card-actionable:not(.disabled):hover {
box-shadow: var(--defaultShadow);
transform: translateY(-2px);
}
-.card-plan-actionable.selected {
+.radio-card-actionable.selected {
border-color: var(--darkBlue);
}
-.card-plan-actionable.selected .card-plan-recommended {
+.radio-card-actionable.selected .radio-card-recommended {
border: solid 1px var(--darkBlue);
border-top: none;
}
-.card-plan-actionable.disabled {
+.radio-card-actionable.disabled {
cursor: not-allowed;
background-color: var(--disableGrayBg);
border-color: var(--disableGrayBorder);
}
-.card-plan-actionable.disabled h2,
-.card-plan-actionable.disabled ul {
+.radio-card-actionable.disabled h2,
+.radio-card-actionable.disabled ul {
color: var(--disableGrayText);
}
-.card-plan-header {
+.radio-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0;
}
-.card-plan-price {
- font-size: var(--bigFontSize);
-}
-
-.card-plan-body {
+.radio-card-body {
flex-grow: 1;
display: flex;
flex-direction: column;
@@ -101,11 +97,11 @@
padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
}
-.card-plan-body .alert {
+.radio-card-body .alert {
margin-bottom: 0;
}
-.card-plan-recommended {
+.radio-card-recommended {
position: relative;
padding: 6px calc(var(--gridSize) * 2);
left: -1px;
diff --git a/server/sonar-web/src/main/js/components/controls/RadioCard.tsx b/server/sonar-web/src/main/js/components/controls/RadioCard.tsx
new file mode 100644
index 00000000000..96726369abf
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/RadioCard.tsx
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { FormattedMessage } from 'react-intl';
+import RecommendedIcon from '../icons-components/RecommendedIcon';
+import { translate } from '../../helpers/l10n';
+import './RadioCard.css';
+
+export interface RadioCardProps {
+ className?: string;
+ disabled?: boolean;
+ onClick?: () => void;
+ selected?: boolean;
+}
+
+interface Props extends RadioCardProps {
+ children: React.ReactNode;
+ recommended?: string;
+ title: React.ReactNode;
+ titleInfo?: React.ReactNode;
+}
+
+export default function RadioCard(props: Props) {
+ const { className, disabled, onClick, recommended, selected, titleInfo } = props;
+ const isActionable = Boolean(onClick);
+ return (
+ <div
+ aria-checked={selected}
+ className={classNames(
+ 'radio-card',
+ { 'radio-card-actionable': isActionable, disabled, selected },
+ className
+ )}
+ onClick={isActionable && !disabled ? onClick : undefined}
+ role="radio"
+ tabIndex={0}>
+ <h2 className="radio-card-header big-spacer-bottom">
+ <span className="display-flex-center">
+ {isActionable && (
+ <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
+ )}
+ {props.title}
+ </span>
+ {titleInfo}
+ </h2>
+ <div className="radio-card-body">{props.children}</div>
+ {recommended && (
+ <div className="radio-card-recommended">
+ <RecommendedIcon className="spacer-right" />
+ <FormattedMessage
+ defaultMessage={recommended}
+ id={recommended}
+ values={{ recommended: <strong>{translate('recommended')}</strong> }}
+ />
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
index a8a1ba1fe2d..9870f0e6ec7 100644
--- a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Modal from './Modal';
+import Modal, { ModalProps } from './Modal';
export interface ChildrenProps {
onCloseClick: (event?: React.SyntheticEvent<HTMLElement>) => void;
@@ -27,7 +27,7 @@ export interface ChildrenProps {
submitting: boolean;
}
-interface Props {
+interface Props extends ModalProps {
children: (props: ChildrenProps) => React.ReactNode;
header: string;
onClose: () => void;
@@ -86,9 +86,10 @@ export default class SimpleModal extends React.Component<Props, State> {
};
render() {
+ const { children, header, onClose, onSubmit, ...modalProps } = this.props;
return (
- <Modal contentLabel={this.props.header} onRequestClose={this.props.onClose}>
- {this.props.children({
+ <Modal contentLabel={header} onRequestClose={onClose} {...modalProps}>
+ {children({
onCloseClick: this.handleCloseClick,
onFormSubmit: this.handleFormSubmit,
onSubmitClick: this.handleSubmitClick,
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx
index f44e5f5e3f6..da11ea3458c 100644
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx
@@ -19,15 +19,15 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import CardPlan, { FreeCardPlan, PaidCardPlan } from '../CardPlan';
-import { click } from '../../../../helpers/testUtils';
+import RadioCard from '../RadioCard';
+import { click } from '../../../helpers/testUtils';
it('should render correctly', () => {
expect(
shallow(
- <CardPlan recommended="Recommended for you" startingPrice={10} title="Paid Plan">
+ <RadioCard recommended="Recommended for you" title="Radio Card" titleInfo="info">
<div>content</div>
- </CardPlan>
+ </RadioCard>
)
).toMatchSnapshot();
});
@@ -35,37 +35,13 @@ it('should render correctly', () => {
it('should be actionable', () => {
const onClick = jest.fn();
const wrapper = shallow(
- <CardPlan onClick={onClick} title="Free Plan">
+ <RadioCard onClick={onClick} title="Radio Card">
<div>content</div>
- </CardPlan>
+ </RadioCard>
);
expect(wrapper).toMatchSnapshot();
click(wrapper);
- wrapper.setProps({ selected: true, startingPrice: 0 });
+ wrapper.setProps({ selected: true, titleInfo: 'info' });
expect(wrapper).toMatchSnapshot();
});
-
-describe('#FreeCardPlan', () => {
- it('should render', () => {
- expect(shallow(<FreeCardPlan hasWarning={false} />)).toMatchSnapshot();
- });
-
- it('should render with warning', () => {
- expect(
- shallow(<FreeCardPlan almName="GitHub" hasWarning={true} selected={true} />)
- ).toMatchSnapshot();
- });
-
- it('should render disabled with info', () => {
- expect(
- shallow(<FreeCardPlan almName="GitHub" disabled={true} hasWarning={false} />)
- ).toMatchSnapshot();
- });
-});
-
-describe('#PaidCardPlan', () => {
- it('should render', () => {
- expect(shallow(<PaidCardPlan isRecommended={true} startingPrice={10} />)).toMatchSnapshot();
- });
-});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap
new file mode 100644
index 00000000000..4394ef55ac3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should be actionable 1`] = `
+<div
+ className="radio-card radio-card-actionable"
+ onClick={[MockFunction]}
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right"
+ />
+ Radio Card
+ </span>
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should be actionable 2`] = `
+<div
+ aria-checked={true}
+ className="radio-card radio-card-actionable selected"
+ onClick={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "currentTarget": Object {
+ "blur": [Function],
+ },
+ "preventDefault": [Function],
+ "stopPropagation": [Function],
+ "target": Object {
+ "blur": [Function],
+ },
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right is-checked"
+ />
+ Radio Card
+ </span>
+ info
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="radio-card"
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ Radio Card
+ </span>
+ info
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+ <div
+ className="radio-card-recommended"
+ >
+ <RecommendedIcon
+ className="spacer-right"
+ />
+ <FormattedMessage
+ defaultMessage="Recommended for you"
+ id="Recommended for you"
+ values={
+ Object {
+ "recommended": <strong>
+ recommended
+ </strong>,
+ }
+ }
+ />
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/hoc/utils.ts b/server/sonar-web/src/main/js/components/hoc/utils.ts
index e324bcf7757..6548cae6a21 100644
--- a/server/sonar-web/src/main/js/components/hoc/utils.ts
+++ b/server/sonar-web/src/main/js/components/hoc/utils.ts
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-export function getWrappedDisplayName(WrappedComponent: React.ComponentClass, hocName: string) {
+export function getWrappedDisplayName(WrappedComponent: React.ComponentType, hocName: string) {
const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
return `${hocName}(${wrappedDisplayName})`;
}
diff --git a/server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx b/server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx
index 8a11dbe7428..0ea58a41e18 100644
--- a/server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx
+++ b/server/sonar-web/src/main/js/components/hoc/withCurrentUser.tsx
@@ -23,7 +23,7 @@ import { getWrappedDisplayName } from './utils';
import { Store, getCurrentUser } from '../../store/rootReducer';
export function withCurrentUser<P>(
- WrappedComponent: React.ComponentClass<P & { currentUser: T.CurrentUser }>
+ WrappedComponent: React.ComponentType<P & { currentUser: T.CurrentUser }>
) {
class Wrapper extends React.Component<P & { currentUser: T.CurrentUser }> {
static displayName = getWrappedDisplayName(WrappedComponent, 'withCurrentUser');
diff --git a/server/sonar-web/src/main/js/components/ui/NewInfoBox.css b/server/sonar-web/src/main/js/components/ui/NewInfoBox.css
new file mode 100644
index 00000000000..0c8b0ca2a20
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/NewInfoBox.css
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.new-info-box {
+ display: flex;
+ padding: var(--gridSize);
+ background-color: var(--veryLightBlue);
+ border: 1px solid var(--alertBorderInfo);
+ border-radius: 2px;
+}
+
+.new-info-box-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
diff --git a/server/sonar-web/src/main/js/components/ui/NewInfoBox.tsx b/server/sonar-web/src/main/js/components/ui/NewInfoBox.tsx
new file mode 100644
index 00000000000..c9a2da3e401
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/NewInfoBox.tsx
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { ButtonIcon } from './buttons';
+import ClearIcon from '../icons-components/ClearIcon';
+import { sonarcloudBlack500 } from '../../app/theme';
+import { translate } from '../../helpers/l10n';
+import './NewInfoBox.css';
+
+export interface Props {
+ children: React.ReactNode;
+ className?: string;
+ description: React.ReactNode;
+ onClose?: () => void;
+ title: string;
+}
+
+export default function NewInfoBox({ className, children, description, onClose, title }: Props) {
+ return (
+ <div className={classNames('new-info-box', className)} role="alert">
+ <div className="new-info-box-inner text-left">
+ <div className="new-info-box-header spacer-bottom">
+ <span className="display-inline-flex-center">
+ <span className="badge badge-new spacer-right">{translate('new')}</span>
+ <strong>{title}</strong>
+ </span>
+ </div>
+ <p className="note spacer-bottom">{description}</p>
+ {children}
+ </div>
+ <ButtonIcon className="button-small spacer-left" color={sonarcloudBlack500} onClick={onClose}>
+ <ClearIcon size={12} />
+ </ButtonIcon>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/NewInfoBox-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/NewInfoBox-test.tsx
new file mode 100644
index 00000000000..d7fc93c6a11
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/NewInfoBox-test.tsx
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import NewInfoBox from '../NewInfoBox';
+import { click } from '../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <NewInfoBox description="My description" onClose={jest.fn()} title="My title">
+ <div />
+ </NewInfoBox>
+ )
+ ).toMatchSnapshot();
+});
+
+it('should allow to opt out', () => {
+ const onClose = jest.fn();
+ const wrapper = shallow(
+ <NewInfoBox description="" onClose={onClose} title="">
+ <div />
+ </NewInfoBox>
+ );
+ click(wrapper.find('ButtonIcon'));
+ expect(onClose).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewInfoBox-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewInfoBox-test.tsx.snap
new file mode 100644
index 00000000000..fca669ba00a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/NewInfoBox-test.tsx.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="new-info-box"
+ role="alert"
+>
+ <div
+ className="new-info-box-inner text-left"
+ >
+ <div
+ className="new-info-box-header spacer-bottom"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <span
+ className="badge badge-new spacer-right"
+ >
+ new
+ </span>
+ <strong>
+ My title
+ </strong>
+ </span>
+ </div>
+ <p
+ className="note spacer-bottom"
+ >
+ My description
+ </p>
+ <div />
+ </div>
+ <ButtonIcon
+ className="button-small spacer-left"
+ color="#8a8c8f"
+ onClick={[MockFunction]}
+ >
+ <ClearIcon
+ size={12}
+ />
+ </ButtonIcon>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
index 842ac795cb7..877de0ca6c8 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
@@ -17,7 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { isBitbucket, isGithub, isPersonal, isVSTS, sanitizeAlmId } from '../almIntegrations';
+import {
+ isBitbucket,
+ isGithub,
+ isPersonal,
+ isVSTS,
+ sanitizeAlmId,
+ getAlmMembersUrl
+} from '../almIntegrations';
+
+it('#getAlmMembersUrl', () => {
+ expect(
+ getAlmMembersUrl({ key: 'github', membersSync: true, url: 'https://github.com/Foo' })
+ ).toBe('https://github.com/orgs/Foo/people');
+ expect(
+ getAlmMembersUrl({ key: 'bitbucket', membersSync: true, url: 'https://bitbucket.com/Foo/' })
+ ).toBe('https://bitbucket.com/Foo/profile/members');
+});
it('#isBitbucket', () => {
expect(isBitbucket('bitbucket')).toBeTruthy();
diff --git a/server/sonar-web/src/main/js/helpers/almIntegrations.ts b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
index 650e0785581..ebeccc800f4 100644
--- a/server/sonar-web/src/main/js/helpers/almIntegrations.ts
+++ b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
@@ -19,6 +19,16 @@
*/
import { isLoggedIn } from './users';
+export function getAlmMembersUrl({ key, url }: T.OrganizationAlm): string {
+ if (!url.endsWith('/')) {
+ url += '/';
+ }
+ if (isGithub(key)) {
+ return url.replace('github.com/', 'github.com/orgs/') + 'people';
+ }
+ return url + 'profile/members';
+}
+
export function hasAdvancedALMIntegration(user: T.CurrentUser) {
return (
isLoggedIn(user) && (isBitbucket(user.externalProvider) || isGithub(user.externalProvider))
diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts
index 4a7fd47182b..9f7ec979805 100644
--- a/server/sonar-web/src/main/js/helpers/testMocks.ts
+++ b/server/sonar-web/src/main/js/helpers/testMocks.ts
@@ -54,9 +54,13 @@ export function mockComponent(overrides: Partial<T.Component> = {}): T.Component
};
}
-export function mockCurrentUser(overrides: Partial<T.CurrentUser> = {}): T.CurrentUser {
+export function mockCurrentUser(overrides: Partial<T.LoggedInUser> = {}): T.LoggedInUser {
return {
+ groups: [],
isLoggedIn: true,
+ login: 'luke',
+ name: 'Skywalker',
+ scmAccounts: [],
...overrides
};
}
@@ -84,11 +88,24 @@ export function mockLocation(overrides: Partial<Location> = {}): Location {
}
export function mockOrganization(overrides: Partial<T.Organization> = {}): T.Organization {
- return {
- key: 'foo',
- name: 'Foo',
+ return { key: 'foo', name: 'Foo', ...overrides };
+}
+
+export function mockOrganizationWithAdminActions(
+ overrides: Partial<T.Organization> = {},
+ actionsOverrides: Partial<T.Organization['actions']> = {}
+) {
+ return mockOrganization({ actions: { admin: true, ...actionsOverrides }, ...overrides });
+}
+
+export function mockOrganizationWithAlm(
+ overrides: Partial<T.Organization> = {},
+ almOverrides: Partial<T.Organization['alm']> = {}
+): T.Organization {
+ return mockOrganization({
+ alm: { key: 'github', membersSync: false, url: 'https://github.com/foo', ...almOverrides },
...overrides
- };
+ });
}
export function mockQualityProfile(overrides: Partial<Profile> = {}): Profile {