From: Grégoire Aubert Date: Tue, 5 Feb 2019 08:38:19 +0000 (+0100) Subject: SONARCLOUD-379 Enable users sync on existing ALM bound organizations X-Git-Tag: 7.7~116 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=9bd42df0365a3c64161ac9283c62b4a9f422402d;p=sonarqube.git 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 --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java index 775b61b0aa6..116b2afca93 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java @@ -57,7 +57,7 @@ public class SetSettingAction implements UsersWsAction { .setRequired(true) .setMaximumLength(100) .setDescription("Setting key") - .setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE); + .setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE, "organizations.members.dismissSyncNotif"); action.createParam(PARAM_VALUE) .setRequired(true) diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java index ba8d78ea535..f2175a39a03 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java @@ -91,11 +91,17 @@ public class SetSettingActionTest { .setParam("value", "true") .execute(); + ws.newRequest() + .setParam("key", "organizations.members.dismissSyncNotif") + .setParam("value", "org1,org2") + .execute(); + assertThat(db.getDbClient().userPropertiesDao().selectByUser(db.getSession(), user)) .extracting(UserPropertyDto::getKey, UserPropertyDto::getValue) .containsExactlyInAnyOrder( tuple("notifications.readDate", "1234"), - tuple("notifications.optOut", "true")); + tuple("notifications.optOut", "true"), + tuple("organizations.members.dismissSyncNotif", "org1,org2")); } @Test @@ -123,7 +129,10 @@ public class SetSettingActionTest { tuple("key", true, 100), tuple("value", true, 4000)); - assertThat(definition.param("key").possibleValues()).containsExactlyInAnyOrder("notifications.optOut", "notifications.readDate"); + assertThat(definition.param("key").possibleValues()).containsExactlyInAnyOrder( + "notifications.optOut", + "notifications.readDate", + "organizations.members.dismissSyncNotif"); } } 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 { 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 { <>
  • - {translate('new')} + {translate('new')} {lastNews.notification}
  • 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" > new 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.css b/server/sonar-web/src/main/js/apps/create/components/CardPlan.css deleted file mode 100644 index 174e3c33d00..00000000000 --- a/server/sonar-web/src/main/js/apps/create/components/CardPlan.css +++ /dev/null @@ -1,119 +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. - */ -.card-plan { - display: flex; - flex-direction: column; - width: 450px; - height: 210px; - background-color: #fff; - border: solid 1px var(--barBorderColor); - border-radius: 3px; - box-sizing: border-box; - margin-right: calc(2 * var(--gridSize)); - transition: all 0.2s ease; -} - -.card-plan.animated { - height: 0; - border-width: 0; - overflow: hidden; -} - -.card-plan.animated.open { - height: 210px; - border-width: 1px; -} - -.card-plan.highlight { - box-shadow: var(--defaultShadow); -} - -.card-plan:last-child { - margin-right: 0; -} - -.card-plan:focus { - outline: none; -} - -.card-plan-actionable { - cursor: pointer; -} - -.card-plan-actionable:not(.disabled):hover { - box-shadow: var(--defaultShadow); - transform: translateY(-2px); -} - -.card-plan-actionable.selected { - border-color: var(--darkBlue); -} - -.card-plan-actionable.selected .card-plan-recommended { - border: solid 1px var(--darkBlue); - border-top: none; -} - -.card-plan-actionable.disabled { - cursor: not-allowed; - background-color: var(--disableGrayBg); - border-color: var(--disableGrayBorder); -} - -.card-plan-actionable.disabled h2, -.card-plan-actionable.disabled ul { - color: var(--disableGrayText); -} - -.card-plan-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 { - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize)); -} - -.card-plan-body .alert { - margin-bottom: 0; -} - -.card-plan-recommended { - position: relative; - padding: 6px calc(var(--gridSize) * 2); - left: -1px; - bottom: -1px; - width: 450px; - color: #fff; - background-color: var(--blue); - border-radius: 0 0 3px 3px; - box-sizing: border-box; - font-size: var(--smallFontSize); -} 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 ( -
    -

    - - {isActionable && ( - - )} - {props.title} - - {startingPrice !== undefined && - (startingPrice ? ( - {formatPrice(startingPrice)} - }} - /> - ) : ( - {formatPrice(0)} - ))} -

    -
    {props.children}
    - {recommended && ( -
    - - {translate('recommended')} }} - /> -
    - )} -
    - ); -} - -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 ( - - <> -
    -
      -
    • - {translate('billing.free_plan.all_projects_analyzed_public')} -
    • -
    • {translate('billing.free_plan.anyone_can_browse_source_code')}
    • -
    -
    - {showWarning && ( - - - - )} - {showInfo && ( - - - - )} - -
    - ); -} - -interface PaidProps extends Props { - isRecommended: boolean; -} - -export function PaidCardPlan({ isRecommended, ...props }: PaidProps) { - return ( - - <> - -
    - - {translate('billing.pricing.learn_more')} - -
    - -
    - ); -} 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 ( + +
    +
      +
    • + {translate('billing.free_plan.all_projects_analyzed_public')} +
    • +
    • {translate('billing.free_plan.anyone_can_browse_source_code')}
    • +
    +
    + {showWarning && ( + + + + )} + {showInfo && ( + + + + )} +
    + ); +} 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 ( + {formatPrice(startingPrice)} + }} + /> + ) + } + {...props}> + +
    + + {translate('billing.pricing.learn_more')} + +
    +
    + ); +} 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 - + title={translate('billing.upgrade_box.header')} + titleInfo={ + startingPrice !== undefined && ( + {formatPrice(startingPrice)} + }} + /> + ) + }> <>
    @@ -99,7 +111,7 @@ export default class UpgradeOrganizationBox extends React.PureComponent
    -
    + {upgradeOrganizationModal && ( { - expect( - shallow( - -
    content
    -
    - ) - ).toMatchSnapshot(); -}); - -it('should be actionable', () => { - const onClick = jest.fn(); - const wrapper = shallow( - -
    content
    -
    - ); - - expect(wrapper).toMatchSnapshot(); - click(wrapper); - wrapper.setProps({ selected: true, startingPrice: 0 }); - expect(wrapper).toMatchSnapshot(); -}); - -describe('#FreeCardPlan', () => { - it('should render', () => { - expect(shallow()).toMatchSnapshot(); - }); - - it('should render with warning', () => { - expect( - shallow() - ).toMatchSnapshot(); - }); - - it('should render disabled with info', () => { - expect( - shallow() - ).toMatchSnapshot(); - }); -}); - -describe('#PaidCardPlan', () => { - it('should render', () => { - expect(shallow()).toMatchSnapshot(); - }); -}); 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()).toMatchSnapshot(); +}); + +it('should render with warning', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('should render disabled with info', () => { + expect( + shallow() + ).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()).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`] = ` - -
    -
      -
    • - billing.free_plan.all_projects_analyzed_public -
    • -
    • - billing.free_plan.anyone_can_browse_source_code -
    • -
    -
    -
    -`; - -exports[`#FreeCardPlan should render disabled with info 1`] = ` - -
    -
      -
    • - billing.free_plan.all_projects_analyzed_public -
    • -
    • - billing.free_plan.anyone_can_browse_source_code -
    • -
    -
    - - - -
    -`; - -exports[`#FreeCardPlan should render with warning 1`] = ` - -
    -
      -
    • - billing.free_plan.all_projects_analyzed_public -
    • -
    • - billing.free_plan.anyone_can_browse_source_code -
    • -
    -
    - - - -
    -`; - -exports[`#PaidCardPlan should render 1`] = ` - - -
    - - billing.pricing.learn_more - -
    -
    -`; - -exports[`should be actionable 1`] = ` -
    -

    - - - Free Plan - -

    -
    -
    - content -
    -
    -
    -`; - -exports[`should be actionable 2`] = ` -
    -

    - - - Free Plan - - - billing.price_format.0 - -

    -
    -
    - content -
    -
    -
    -`; - -exports[`should render correctly 1`] = ` -
    -

    - - Paid Plan - - - billing.price_format.10 - , - } - } - /> -

    -
    -
    - content -
    -
    -
    - - - recommended - , - } - } - /> -
    -
    -`; 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`] = ` + +
    +
      +
    • + billing.free_plan.all_projects_analyzed_public +
    • +
    • + billing.free_plan.anyone_can_browse_source_code +
    • +
    +
    +
    +`; + +exports[`should render disabled with info 1`] = ` + +
    +
      +
    • + billing.free_plan.all_projects_analyzed_public +
    • +
    • + billing.free_plan.anyone_can_browse_source_code +
    • +
    +
    + + + +
    +`; + +exports[`should render with warning 1`] = ` + +
    +
      +
    • + billing.free_plan.all_projects_analyzed_public +
    • +
    • + billing.free_plan.anyone_can_browse_source_code +
    • +
    +
    + + + +
    +`; 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`] = ` + + billing.price_format.10 + , + } + } + /> + } +> + +
    + + billing.pricing.learn_more + +
    +
    +`; 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`] = ` - + billing.price_format.10 + , + } + } + /> + } >
    -
    +
    `; 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 }

    - {almKey - ? translate('onboarding.import_organization.installing', almKey) - : translate('onboarding.import_organization.installing')} + {translate( + 'onboarding.import_organization.installing', + sanitizeAlmId(almKey) || 'ALM' + )}

    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()).toMatchSnapshot(); + expect(shallow()).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 = {}) { 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`] = ` + +
    + +

    + onboarding.import_organization.installing.ALM +

    +
    + + } + timeout={100} +/> +`; + +exports[`should render correctly 2`] = ` + +
    + +

    + onboarding.import_organization.installing.github +

    +
    + + } + 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).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 = {}) { @@ -61,8 +50,11 @@ function shallowRender(props: Partial = {}) { ); 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 (
    {formatMeasure(total, 'INT')} {translate('organization.members.members')} + {organization.alm && + organization.alm.membersSync && ( + +

    + {translate( + 'organization.members.auto_sync_total_help', + sanitizeAlmId(organization.alm.key) || '' + )} +

    + {currentUser.personalOrganization !== organization.key && ( + <> +
    +

    + + {translateWithParameters( + 'organization.members.see_all_members_on_x', + translate(sanitizeAlmId(organization.alm.key) || '') + )} + +

    + + )} +
    + } + /> + )} )} 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 ( -
    -

    {translate('organization.members.page')}

    - - {props.children} -

    - - {translate('organization.members.manage_a_team')} - - ) - }} - /> -

    -
    - ); +export class MembersPageHeader extends React.PureComponent { + 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 ( +
    +

    {translate('organization.members.page')}

    + + {isAdmin && ( +
    + {almKey && !showSyncNotif && } + {!hasMemberSync && ( +
    + + +
    + )} + {almKey && + showSyncNotif && ( + + + + )} +
    + )} +
    + + {translate('organization.members.manage_a_team')} + + ) + }} + /> +
    +
    + ); + } } + +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 member.login) : []; return (
    - - {organization.actions && - organization.actions.admin && ( -
    - - -
    - )} -
    + {members !== undefined && paging !== undefined && ( <> - + { 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 { + 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')} + + {translate('learn_more')} + +
    + +
    +
      +
    • + {translate('organization.members.management.manual.add_members_manually')} +
    • +
    • + {translate('organization.members.management.manual.choose_members_permissions')} +
    • +
    +
    +
    + +
    +
      + {almKey && ( + <> +
    • + {translate( + 'organization.members.management.automatic.synchronized_from', + almKey + )} +
    • +
    • + {translate( + 'organization.members.management.automatic.members_changes_reflected', + almKey + )} +
    • + + )} +
    • + {translate( + 'organization.members.management.automatic.still_choose_members_permissions' + )} +
    • +
    +
    + {(!organization.alm || !organization.alm.membersSync) && ( + + {translate('organization.members.management.automatic.warning')} + + )} +
    +
    + + ); + }; + + render() { + const { organization } = this.props; + const orgMemberSync = Boolean(organization.alm && organization.alm.membersSync); + return ( + + {({ onClick }) => ( + + )} + + ); + } +} + +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(); - expect(wrapper).toMatchSnapshot(); + expect(shallowRender({ total: undefined })).toMatchSnapshot(); }); it('should render with the total', () => { - const wrapper = shallow(); - 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 = {}) { + return shallow( + + ); +} 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( - - children test - - ); - 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 = {}) { + return shallow( + + ); +} 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(); + 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( - - ); +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( - - ); + const wrapper = shallowRender(); await waitAndUpdate(wrapper); wrapper.find('MembersListHeader').prop('handleSearch')('user'); expect(searchMembers).lastCalledWith({ organization: 'foo', ps: 50, q: 'user' }); }); it('should load more members', async () => { - const wrapper = shallow( - - ); + const wrapper = shallowRender(); await waitAndUpdate(wrapper); wrapper.find('ListFooter').prop('loadMore')(); expect(searchMembers).lastCalledWith({ organization: 'foo', p: 2, ps: 50, q: undefined }); }); it('should add new member', async () => { - const wrapper = shallow( - - ); + const wrapper = shallowRender(); await waitAndUpdate(wrapper); - wrapper.find('AddMemberForm').prop('addMember')({ login: 'bar' }); + wrapper.find('Connect(MembersPageHeader)').prop('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( - - ); + const wrapper = shallowRender(); await waitAndUpdate(wrapper); wrapper.find('MembersList').prop('removeMember')({ login: 'john' }); await waitAndUpdate(wrapper); @@ -127,9 +119,7 @@ it('should remove member', async () => { }); it('should update groups', async () => { - const wrapper = shallow( - - ); + const wrapper = shallowRender(); await waitAndUpdate(wrapper); wrapper.find('MembersList').prop('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 = {}) { + return shallow( + + ); +} 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('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('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('onConfirm')(); + expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: false }); + + await waitAndUpdate(wrapper); + expect(fetchOrganization).toHaveBeenCalledWith('foo'); + expect(syncMembers).not.toHaveBeenCalled(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} 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`] = ` + +

    + organization.members.auto_sync_total_help.github +

    +
    + } +/> +`; + +exports[`should render a help tooltip 1`] = ` + +

    + organization.members.auto_sync_total_help.github +

    + +
    +

    + + organization.members.see_all_members_on_x.github + +

    +
    + + } +/> +`; + +exports[`should render a help tooltip 2`] = ` + +

    + organization.members.auto_sync_total_help.bitbucket +

    + +
    +

    + + organization.members.see_all_members_on_x.bitbucket + +

    +
    + + } +/> +`; + exports[`should render with the total 1`] = `
    @@ -13,10 +13,7 @@ exports[`should render 1`] = ` loading={true} timeout={100} /> - - children test - -

    -

    +
    + +`; + +exports[`should render for admin 1`] = ` +
    +

    + organization.members.page +

    + +
    +
    + + +
    +
    +
    + + organization.members.manage_a_team + , + } + } + /> +
    +
    +`; + +exports[`should render for bound organization without sync 1`] = ` +
    +

    + organization.members.page +

    + +
    +
    + + +
    + + + +
    +
    + + organization.members.manage_a_team + , + } + } + /> +
    `; 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`] = `
    @@ -12,110 +12,15 @@ exports[`should fetch members and groups and render for admin 1`] = ` - -
    - - -
    -
    - - - -
    -`; - -exports[`should fetch members and render for non-admin 1`] = ` -
    - - -
    `; @@ -132,11 +37,49 @@ exports[`should fetch members and render for non-admin 2`] = ` - + organization.members.management.description + + learn_more + +
    + +
    +
      +
    • + organization.members.management.manual.add_members_manually +
    • +
    • + organization.members.management.manual.choose_members_permissions +
    • +
    +
    +
    + +
    +
      + +
    • + organization.members.management.automatic.synchronized_from.bitbucket +
    • +
    • + organization.members.management.automatic.members_changes_reflected.bitbucket +
    • +
      +
    • + organization.members.management.automatic.still_choose_members_permissions +
    • +
    +
    + + organization.members.management.automatic.warning + +
    +
    + + } + modalHeader="organization.members.management.title" + onConfirm={[Function]} +> + + +`; + +exports[`should allow to switch to automatic mode with github 1`] = ` + + organization.members.management.description + + learn_more + +
    + +
    +
      +
    • + organization.members.management.manual.add_members_manually +
    • +
    • + organization.members.management.manual.choose_members_permissions +
    • +
    +
    +
    + +
    +
      + +
    • + organization.members.management.automatic.synchronized_from.github +
    • +
    • + organization.members.management.automatic.members_changes_reflected.github +
    • +
      +
    • + organization.members.management.automatic.still_choose_members_permissions +
    • +
    +
    + + organization.members.management.automatic.warning + +
    +
    + + } + modalHeader="organization.members.management.title" + onConfirm={[Function]} +> + +
    +`; + +exports[`should allow to switch to manual mode 1`] = ` + + organization.members.management.description + + learn_more + +
    + +
    +
      +
    • + organization.members.management.manual.add_members_manually +
    • +
    • + organization.members.management.manual.choose_members_permissions +
    • +
    +
    +
    + +
    +
      + +
    • + organization.members.management.automatic.synchronized_from.github +
    • +
    • + organization.members.management.automatic.members_changes_reflected.github +
    • +
      +
    • + organization.members.management.automatic.still_choose_members_permissions +
    • +
    +
    +
    +
    + + } + modalHeader="organization.members.management.title" + onConfirm={[Function]} +> + +
    +`; 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( ) 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 extends ConfirmModalProps { 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; } interface State { modal: boolean; } -export default class ConfirmButton extends React.PureComponent { +export default class ConfirmButton extends React.PureComponent, State> { renderConfirmModal = ({ onClose }: ModalProps) => { + const { children, modalBody, modalHeader, ...confirmModalProps } = this.props; return ( - - {this.props.modalBody} + + {modalBody} ); }; 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 { - children: React.ReactNode; +export interface ConfirmModalProps extends ModalProps { cancelButtonText?: string; confirmButtonText: string; confirmData?: T; confirmDisable?: boolean; - header: string; isDestructive?: boolean; + onConfirm: (data?: T) => void | Promise; +} + +interface Props extends ConfirmModalProps { + header: string; onClose: () => void; - onConfirm: (data?: T) => void | Promise; } export default class ConfirmModal extends React.PureComponent> { @@ -82,9 +85,10 @@ export default class ConfirmModal extends React.PureComponent + {this.renderModalContent} ); 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; -type Props = Partial & MandatoryProps & OwnProps; +type Props = Partial & MandatoryProps & ModalProps; export default function Modal(props: Props) { return ( diff --git a/server/sonar-web/src/main/js/components/controls/RadioCard.css b/server/sonar-web/src/main/js/components/controls/RadioCard.css new file mode 100644 index 00000000000..a5099a9a239 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/RadioCard.css @@ -0,0 +1,115 @@ +/* + * 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. + */ +.radio-card { + display: flex; + flex-direction: column; + width: 450px; + min-height: 210px; + background-color: #fff; + border: solid 1px var(--barBorderColor); + border-radius: 3px; + box-sizing: border-box; + margin-right: calc(2 * var(--gridSize)); + transition: all 0.2s ease; +} + +.radio-card.animated { + height: 0; + border-width: 0; + overflow: hidden; +} + +.radio-card.animated.open { + height: 210px; + border-width: 1px; +} + +.radio-card.highlight { + box-shadow: var(--defaultShadow); +} + +.radio-card:last-child { + margin-right: 0; +} + +.radio-card:focus { + outline: none; +} + +.radio-card-actionable { + cursor: pointer; +} + +.radio-card-actionable:not(.disabled):hover { + box-shadow: var(--defaultShadow); + transform: translateY(-2px); +} + +.radio-card-actionable.selected { + border-color: var(--darkBlue); +} + +.radio-card-actionable.selected .radio-card-recommended { + border: solid 1px var(--darkBlue); + border-top: none; +} + +.radio-card-actionable.disabled { + cursor: not-allowed; + background-color: var(--disableGrayBg); + border-color: var(--disableGrayBorder); +} + +.radio-card-actionable.disabled h2, +.radio-card-actionable.disabled ul { + color: var(--disableGrayText); +} + +.radio-card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0; +} + +.radio-card-body { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize)); +} + +.radio-card-body .alert { + margin-bottom: 0; +} + +.radio-card-recommended { + position: relative; + padding: 6px calc(var(--gridSize) * 2); + left: -1px; + bottom: -1px; + width: 450px; + color: #fff; + background-color: var(--blue); + border-radius: 0 0 3px 3px; + box-sizing: border-box; + font-size: var(--smallFontSize); +} 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 ( +
    +

    + + {isActionable && ( + + )} + {props.title} + + {titleInfo} +

    +
    {props.children}
    + {recommended && ( +
    + + {translate('recommended')} }} + /> +
    + )} +
    + ); +} 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) => 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 { }; render() { + const { children, header, onClose, onSubmit, ...modalProps } = this.props; return ( - - {this.props.children({ + + {children({ onCloseClick: this.handleCloseClick, onFormSubmit: this.handleFormSubmit, onSubmitClick: this.handleSubmitClick, diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx new file mode 100644 index 00000000000..da11ea3458c --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx @@ -0,0 +1,47 @@ +/* + * 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 RadioCard from '../RadioCard'; +import { click } from '../../../helpers/testUtils'; + +it('should render correctly', () => { + expect( + shallow( + +
    content
    +
    + ) + ).toMatchSnapshot(); +}); + +it('should be actionable', () => { + const onClick = jest.fn(); + const wrapper = shallow( + +
    content
    +
    + ); + + expect(wrapper).toMatchSnapshot(); + click(wrapper); + wrapper.setProps({ selected: true, titleInfo: 'info' }); + expect(wrapper).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`] = ` +
    +

    + + + Radio Card + +

    +
    +
    + content +
    +
    +
    +`; + +exports[`should be actionable 2`] = ` +
    +

    + + + Radio Card + + info +

    +
    +
    + content +
    +
    +
    +`; + +exports[`should render correctly 1`] = ` +
    +

    + + Radio Card + + info +

    +
    +
    + content +
    +
    +
    + + + recommended + , + } + } + /> +
    +
    +`; 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

    ( - WrappedComponent: React.ComponentClass

    + WrappedComponent: React.ComponentType

    ) { class Wrapper extends React.Component

    { 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 ( +

    +
    +
    + + {translate('new')} + {title} + +
    +

    {description}

    + {children} +
    + + + +
    + ); +} 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( + +
    + + ) + ).toMatchSnapshot(); +}); + +it('should allow to opt out', () => { + const onClose = jest.fn(); + const wrapper = shallow( + +
    + + ); + 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`] = ` +
    +
    +
    + + + new + + + My title + + +
    +

    + My description +

    +
    +
    + + + +
    +`; 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 }; } -export function mockCurrentUser(overrides: Partial = {}): T.CurrentUser { +export function mockCurrentUser(overrides: Partial = {}): T.LoggedInUser { return { + groups: [], isLoggedIn: true, + login: 'luke', + name: 'Skywalker', + scmAccounts: [], ...overrides }; } @@ -84,11 +88,24 @@ export function mockLocation(overrides: Partial = {}): Location { } export function mockOrganization(overrides: Partial = {}): T.Organization { - return { - key: 'foo', - name: 'Foo', + return { key: 'foo', name: 'Foo', ...overrides }; +} + +export function mockOrganizationWithAdminActions( + overrides: Partial = {}, + actionsOverrides: Partial = {} +) { + return mockOrganization({ actions: { admin: true, ...actionsOverrides }, ...overrides }); +} + +export function mockOrganizationWithAlm( + overrides: Partial = {}, + almOverrides: Partial = {} +): T.Organization { + return mockOrganization({ + alm: { key: 'github', membersSync: false, url: 'https://github.com/foo', ...almOverrides }, ...overrides - }; + }); } export function mockQualityProfile(overrides: Partial = {}): Profile { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 8427e8ecd10..cbbb966a3a1 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -18,6 +18,7 @@ any=Any ascending=Ascending assignee=Assignee author=Author +bitbucket=Bitbucket back=Back backup=Backup backup_verb=Back up @@ -76,6 +77,7 @@ follow=Follow format=Format from=From global=Global +github=GitHub help=Help hide=Hide inactive=Inactive @@ -2673,6 +2675,25 @@ organization.members.manage_groups=Manage groups organization.members.members_groups={0}'s groups: organization.members.manage_a_team=Manage a team organization.members.add_to_members=Add to members +organization.members.config_synchro=Configure Synchronization +organization.members.auto_sync_with_x=Automatic sync with {0} +organization.members.auto_sync_members_from_org.bitbucket=Members can be synchronized automatically from your Bitbucket team +organization.members.auto_sync_members_from_org.github=Members can be synchronized automatically from your GitHub organization +organization.members.auto_sync_total_help.bitbucket=You might not see all members from your Bitbucket team yet, as they need to reconnect to SonarCloud to be members of the organization. +organization.members.auto_sync_total_help.github=You might not see all members from your GitHub organization yet, as they need to connect to SonarCloud at least once to appear in this list. +organization.members.see_all_members_on_x=See all members on {0} +organization.members.management.title=Members Management +organization.members.management.description=Select your management mode for members of this organization +organization.members.management.manual=Manual +organization.members.management.manual.add_members_manually=Admin add members manually from Sonarcloud existing users +organization.members.management.manual.choose_members_permissions=Admin chooses each member permissions +organization.members.management.automatic=Automatic sync with {0} +organization.members.management.automatic.synchronized_from.bitbucket=Members are synchronized automatically from your Bitbucket team +organization.members.management.automatic.synchronized_from.github=Members are synchronized automatically from your GitHub organization +organization.members.management.automatic.members_changes_reflected.bitbucket=Your team members must reconnect to SonarCloud to be automatically added to correct SonarCloud organization +organization.members.management.automatic.members_changes_reflected.github=If you add or remove a member on GitHub, SonarCloud immediately reflect the changes +organization.members.management.automatic.still_choose_members_permissions=Admin still manages permissions for each member in SonarCloud +organization.members.management.automatic.warning=This will override your current Members and Permissions configuration organization.paid_plan.badge=Paid plan organization.default_visibility_of_new_projects=Default visibility of new projects: organization.change_visibility_form.header=Set Default Visibility of New Projects @@ -2808,13 +2829,11 @@ onboarding.import_organization.org_not_found.tips_2=Try to uninstall and re-inst onboarding.import_organization.choose_organization=Choose an organization... onboarding.import_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket onboarding.import_organization.choose_organization_button.github=Choose an organization on GitHub -onboarding.import_organization.installing=Finalize installation of the ALM application... -onboarding.import_organization.installing.bitbucket=Finalize installation of the Bitbucket application.. -onboarding.import_organization.installing.github=Finalize installation of the GitHub application... +onboarding.import_organization.installing=Finalize installation of the {0} application... onboarding.import_organization.personal.page.header=Bind to your personal organization onboarding.import_organization.personal.import_org_details=Import personal organization details onboarding.import_organization.private.disabled=Selecting private repository is not available yet and will come soon. Meanwhile, you need to create the project manually. -onboarding.import_organization.bitbucket=Import from BitBucket teams +onboarding.import_organization.bitbucket=Import from Bitbucket teams onboarding.import_organization.github=Import from GitHub organizations onboarding.import_organization.bind_existing=Bind to an existing SonarCloud organization onboarding.import_organization.create_new=Create new SonarCloud organization from it