aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2019-02-14 18:22:10 +0100
committersonartech <sonartech@sonarsource.com>2019-03-06 11:30:42 +0100
commitf79ab22a93ead12949592ec5095401db718af63e (patch)
treefee128d48a20e2847c245695393b6134af91736c /server/sonar-web/src/main/js
parent980cb9cb6854597e5adbad6fe2536f87cb519bd1 (diff)
downloadsonarqube-f79ab22a93ead12949592ec5095401db718af63e.tar.gz
sonarqube-f79ab22a93ead12949592ec5095401db718af63e.zip
SONARCLOUD-413 Add organizations list in onboarding modal
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/app/components/StartupModal.tsx3
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/app/theme.js1
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx98
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx52
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx56
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap114
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap111
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap25
-rw-r--r--server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx34
-rw-r--r--server/sonar-web/src/main/js/components/ui/buttons.css27
-rw-r--r--server/sonar-web/src/main/js/components/ui/buttons.tsx10
26 files changed, 733 insertions, 108 deletions
diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
index c5d84e6c8eb..cbc48881b23 100644
--- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx
+++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
@@ -58,7 +58,7 @@ interface WithRouterProps {
type Props = StateProps & DispatchProps & OwnProps & WithRouterProps;
-enum ModalKey {
+export enum ModalKey {
license,
onboarding
}
@@ -153,6 +153,7 @@ export class StartupModal extends React.PureComponent<Props, State> {
<OnboardingModal
onClose={this.closeOnboarding}
onOpenProjectOnboarding={this.openProjectOnboarding}
+ skipOnboarding={this.props.skipOnboarding}
/>
)}
</OnboardingContext.Provider>
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
index b2476e2d50c..e61ab79265a 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
@@ -19,13 +19,14 @@
*/
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
-import { StartupModal } from '../StartupModal';
+import { StartupModal, ModalKey } from '../StartupModal';
import { showLicense } from '../../../api/marketplace';
import { save, get } from '../../../helpers/storage';
import { hasMessage } from '../../../helpers/l10n';
import { waitAndUpdate } from '../../../helpers/testUtils';
import { differenceInDays, toShortNotSoISOString } from '../../../helpers/dates';
import { EditionKey } from '../../../apps/marketplace/utils';
+import { mockOrganization, mockRouter } from '../../../helpers/testMocks';
jest.mock('../../../api/marketplace', () => ({
showLicense: jest.fn().mockResolvedValue(undefined)
@@ -110,6 +111,36 @@ it('should render license prompt', async () => {
await shouldDisplayLicense(getWrapper());
});
+describe('closeOnboarding', () => {
+ it('should set state and skip onboarding', () => {
+ const skipOnboarding = jest.fn();
+ const wrapper = getWrapper({ skipOnboarding });
+
+ wrapper.setState({ modal: ModalKey.onboarding });
+ wrapper.instance().closeOnboarding();
+
+ expect(wrapper.state('modal')).toBe(undefined);
+
+ expect(skipOnboarding).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('openProjectOnboarding', () => {
+ it('should set state and redirect', () => {
+ const push = jest.fn();
+ const wrapper = getWrapper({ router: mockRouter({ push }) });
+
+ wrapper.instance().openProjectOnboarding(mockOrganization());
+
+ expect(wrapper.state('modal')).toBe(undefined);
+
+ expect(push).toHaveBeenCalledWith({
+ pathname: `/projects/create`,
+ state: { organization: 'foo', tab: 'manual' }
+ });
+ });
+});
+
async function shouldNotHaveModals(wrapper: ShallowWrapper) {
await waitAndUpdate(wrapper);
expect(wrapper.find('LicensePromptModal').exists()).toBeFalsy();
@@ -121,7 +152,7 @@ async function shouldDisplayLicense(wrapper: ShallowWrapper) {
}
function getWrapper(props: Partial<StartupModal['props']> = {}) {
- return shallow(
+ return shallow<StartupModal>(
<StartupModal
canAdmin={true}
currentEdition={EditionKey.enterprise}
diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js
index 25ce5c25956..4fd00b72e36 100644
--- a/server/sonar-web/src/main/js/app/theme.js
+++ b/server/sonar-web/src/main/js/app/theme.js
@@ -95,6 +95,7 @@ module.exports = {
bigFontSize: '16px',
hugeFontSize: '24px',
+ hugeControlHeight: `${5 * grid}px`,
largeControlHeight: `${4 * grid}px`,
controlHeight: `${3 * grid}px`,
smallControlHeight: `${2.5 * grid}px`,
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 8871a1933de..9015c4ba8b2 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
@@ -37,6 +37,7 @@ interface Props {
loading: boolean;
members?: T.OrganizationMember[];
organization: T.Organization;
+ refreshMembers: () => Promise<void>;
setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
}
@@ -50,7 +51,7 @@ export class MembersPageHeader extends React.PureComponent<Props> {
};
render() {
- const { dismissSyncNotifOrg, members, organization } = this.props;
+ const { dismissSyncNotifOrg, members, organization, refreshMembers } = 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);
@@ -67,7 +68,10 @@ export class MembersPageHeader extends React.PureComponent<Props> {
<DeferredSpinner loading={this.props.loading} />
{isAdmin && (
<div className="page-actions text-right">
- {almKey && !showSyncNotif && <SyncMemberForm organization={organization} />}
+ {almKey &&
+ !showSyncNotif && (
+ <SyncMemberForm organization={organization} refreshMembers={refreshMembers} />
+ )}
{!hasMemberSync && (
<div className="display-inline-block spacer-left spacer-bottom">
<AddMemberForm
@@ -93,7 +97,11 @@ export class MembersPageHeader extends React.PureComponent<Props> {
'organization.members.auto_sync_with_x',
translate(almKey)
)}>
- <SyncMemberForm organization={organization} />
+ <SyncMemberForm
+ dismissSyncNotif={this.handleDismissSyncNotif}
+ organization={organization}
+ refreshMembers={refreshMembers}
+ />
</NewInfoBox>
)}
</div>
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 46e1162372b..da2ea484d97 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
@@ -151,6 +151,18 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
);
};
+ refreshMembers = () => {
+ return searchMembers({
+ organization: this.props.organization.key,
+ ps: PAGE_SIZE,
+ q: this.state.query || undefined
+ }).then(({ paging, users }) => {
+ if (this.mounted) {
+ this.setState({ members: users, paging });
+ }
+ });
+ };
+
updateGroup = (
login: string,
updater: (member: T.OrganizationMember) => T.OrganizationMember
@@ -195,6 +207,7 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
loading={loading}
members={members}
organization={organization}
+ refreshMembers={this.refreshMembers}
/>
{members !== undefined &&
paging !== undefined && (
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
index 1a7a5a8400e..9ebb9f57e21 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
@@ -30,8 +30,10 @@ import { translate, translateWithParameters } from '../../helpers/l10n';
import { fetchOrganization } from '../../store/rootActions';
interface Props {
+ dismissSyncNotif?: () => void;
fetchOrganization: (key: string) => void;
organization: T.Organization;
+ refreshMembers: () => Promise<void>;
}
interface State {
@@ -47,15 +49,20 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
}
handleConfirm = () => {
- const { organization } = this.props;
+ const { dismissSyncNotif, organization } = this.props;
const { membersSync } = this.state;
+
+ if (dismissSyncNotif) {
+ dismissSyncNotif();
+ }
+
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 this.handleMemberSync();
}
return Promise.resolve();
});
@@ -69,6 +76,10 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
this.setState({ membersSync: true });
};
+ handleMemberSync = () => {
+ return syncMembers(this.props.organization.key).then(this.props.refreshMembers);
+ };
+
renderModalDescription = () => {
return (
<p className="spacer-top">
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 7f74a8eff3c..bb2a10ad805 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
@@ -63,6 +63,7 @@ function shallowRender(props: Partial<MembersPageHeader['props']> = {}) {
loading={false}
members={[]}
organization={mockOrganization()}
+ refreshMembers={jest.fn()}
setCurrentUserSetting={jest.fn()}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
index 7861581aa2e..eac606f79c0 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
@@ -54,8 +54,7 @@ jest.mock('../../../api/user_groups', () => ({
}));
beforeEach(() => {
- (searchMembers as jest.Mock).mockClear();
- (searchUsersGroups as jest.Mock).mockClear();
+ jest.clearAllMocks();
});
it('should fetch members and render for non-admin', async () => {
@@ -88,6 +87,28 @@ it('should load more members', async () => {
expect(searchMembers).lastCalledWith({ organization: 'foo', p: 2, ps: 50, q: undefined });
});
+it('should refresh members', async () => {
+ const paging = { pageIndex: 1, pageSize: 5, total: 3 };
+ const users = [
+ { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
+ { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 },
+ { login: 'stan', name: 'Stan Marsh', avatar: '', groupCount: 7 }
+ ];
+
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ (searchMembers as jest.Mock).mockResolvedValueOnce({
+ paging,
+ users
+ });
+
+ await wrapper.instance().refreshMembers();
+ expect(searchMembers).toBeCalled();
+ expect(wrapper.state('members')).toEqual(users);
+ expect(wrapper.state('paging')).toEqual(paging);
+});
+
it('should add new member', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
@@ -140,7 +161,7 @@ it('should update groups', async () => {
});
function shallowRender(props: Partial<OrganizationMembers['props']> = {}) {
- return shallow(
+ return shallow<OrganizationMembers>(
<OrganizationMembers
currentUser={mockCurrentUser()}
organization={mockOrganizationWithAdminActions()}
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
index 4410d45c301..78dd230299a 100644
--- 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
@@ -35,8 +35,10 @@ beforeEach(() => {
});
it('should allow to switch to automatic mode with github', async () => {
+ const dismissSyncNotif = jest.fn();
const fetchOrganization = jest.fn();
- const wrapper = shallowRender({ fetchOrganization });
+ const refreshMembers = jest.fn().mockResolvedValue({});
+ const wrapper = shallowRender({ dismissSyncNotif, fetchOrganization, refreshMembers });
expect(wrapper).toMatchSnapshot();
wrapper.setState({ membersSync: true });
@@ -46,6 +48,8 @@ it('should allow to switch to automatic mode with github', async () => {
await waitAndUpdate(wrapper);
expect(fetchOrganization).toHaveBeenCalledWith('foo');
expect(syncMembers).toHaveBeenCalledWith('foo');
+ expect(refreshMembers).toBeCalledTimes(1);
+ expect(dismissSyncNotif).toBeCalledTimes(1);
});
it('should allow to switch to automatic mode with bitbucket', async () => {
@@ -87,6 +91,7 @@ function shallowRender(props: Partial<SyncMemberForm['props']> = {}) {
<SyncMemberForm
fetchOrganization={jest.fn()}
organization={mockOrganizationWithAlm()}
+ refreshMembers={jest.fn().mockResolvedValue({})}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
index 93646b3ecc1..0225976a5a6 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
@@ -143,6 +143,7 @@ exports[`should render for bound organization without sync 1`] = `
title="organization.members.auto_sync_with_x.github"
>
<Connect(SyncMemberForm)
+ dismissSyncNotif={[Function]}
organization={
Object {
"actions": Object {
@@ -157,6 +158,7 @@ exports[`should render for bound organization without sync 1`] = `
"name": "Foo",
}
}
+ refreshMembers={[MockFunction]}
/>
</NewInfoBox>
</div>
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 dd26a5928f4..2d451e1086c 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
@@ -21,6 +21,7 @@ exports[`should fetch members and render for non-admin 1`] = `
"name": "Foo",
}
}
+ refreshMembers={[Function]}
/>
</div>
`;
@@ -62,6 +63,7 @@ exports[`should fetch members and render for non-admin 2`] = `
"name": "Foo",
}
}
+ refreshMembers={[Function]}
/>
<MembersListHeader
currentUser={
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
index adf205bd0ea..1163399e78b 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
@@ -18,67 +18,79 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { connect } from 'react-redux';
-import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import OrganizationsShortList from './OrganizationsShortList';
import Modal from '../../../components/controls/Modal';
import OnboardingProjectIcon from '../../../components/icons-components/OnboardingProjectIcon';
+import OnboardingTeamIcon from '../../../components/icons-components/OnboardingTeamIcon';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
+import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations';
import { Button, ResetButtonLink } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-import { isLoggedIn } from '../../../helpers/users';
import '../styles.css';
-interface OwnProps {
+export interface Props {
+ currentUser: T.LoggedInUser;
onClose: () => void;
onOpenProjectOnboarding: () => void;
+ skipOnboarding: () => void;
+ userOrganizations: T.Organization[];
}
-interface StateProps {
- currentUser: T.CurrentUser;
-}
-
-type Props = OwnProps & StateProps;
-
-export class OnboardingModal extends React.PureComponent<Props> {
- componentDidMount() {
- if (!isLoggedIn(this.props.currentUser)) {
- handleRequiredAuthentication();
- }
- }
+export function OnboardingModal(props: Props) {
+ const {
+ currentUser,
+ onClose,
+ onOpenProjectOnboarding,
+ skipOnboarding,
+ userOrganizations
+ } = props;
- render() {
- if (!isLoggedIn(this.props.currentUser)) {
- return null;
- }
+ const organizations = userOrganizations.filter(o => o.key !== currentUser.personalOrganization);
- const header = translate('onboarding.header');
- return (
- <Modal
- contentLabel={header}
- medium={true}
- onRequestClose={this.props.onClose}
- shouldCloseOnOverlayClick={false}>
- <div className="modal-head">
- <h2>{translate('onboarding.header')}</h2>
- <p className="spacer-top">{translate('onboarding.header.description')}</p>
- </div>
- <div className="modal-body text-center huge-spacer-top huge-spacer-bottom">
+ const header = translate('onboarding.header');
+ return (
+ <Modal
+ contentLabel={header}
+ medium={true}
+ onRequestClose={onClose}
+ shouldCloseOnOverlayClick={false}>
+ <div className="modal-head">
+ <h2>{translate('onboarding.header')}</h2>
+ <p className="spacer-top">{translate('onboarding.header.description')}</p>
+ </div>
+ <div className="modal-body text-center display-flex-row huge-spacer-top huge-spacer-bottom">
+ <div className="flex-1">
<OnboardingProjectIcon className="big-spacer-bottom" />
<h6 className="onboarding-choice-name big-spacer-bottom">
{translate('onboarding.analyze_your_code')}
</h6>
- <Button onClick={this.props.onOpenProjectOnboarding}>
+ <Button onClick={onOpenProjectOnboarding}>
{translate('onboarding.project.create')}
</Button>
</div>
- <div className="modal-foot text-right">
- <ResetButtonLink onClick={this.props.onClose}>{translate('not_now')}</ResetButtonLink>
- </div>
- </Modal>
- );
- }
+ {organizations.length > 0 && (
+ <>
+ <div className="vertical-pipe-separator">
+ <div className="vertical-separator" />
+ </div>
+ <div className="flex-1">
+ <OnboardingTeamIcon className="big-spacer-bottom" />
+ <h6 className="onboarding-choice-name big-spacer-bottom">
+ {translate('onboarding.browse_your_organizations')}
+ </h6>
+ <OrganizationsShortList
+ organizations={organizations}
+ skipOnboarding={skipOnboarding}
+ />
+ </div>
+ </>
+ )}
+ </div>
+ <div className="modal-foot text-right">
+ <ResetButtonLink onClick={onClose}>{translate('not_now')}</ResetButtonLink>
+ </div>
+ </Modal>
+ );
}
-const mapStateToProps = (state: Store): StateProps => ({ currentUser: getCurrentUser(state) });
-
-export default connect(mapStateToProps)(OnboardingModal);
+export default withUserOrganizations(whenLoggedIn(OnboardingModal));
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
index 9533287403b..5bda42d861b 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
@@ -19,44 +19,30 @@
*/
import * as React from 'react';
import { connect } from 'react-redux';
-import { InjectedRouter } from 'react-router';
import OnboardingModal from './OnboardingModal';
import { skipOnboarding } from '../../../store/users';
import { OnboardingContext } from '../../../app/components/OnboardingContext';
+import { Router } from '../../../components/hoc/withRouter';
-interface DispatchProps {
+interface Props {
+ router: Router;
skipOnboarding: () => void;
}
-interface OwnProps {
- router: InjectedRouter;
-}
-
-interface State {
- open: boolean;
-}
-
-export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps, State> {
- state: State = { open: false };
-
+export class OnboardingPage extends React.PureComponent<Props> {
closeOnboarding = () => {
this.props.skipOnboarding();
this.props.router.replace('/');
};
render() {
- const { open } = this.state;
-
- if (!open) {
- return null;
- }
-
return (
<OnboardingContext.Consumer>
{openProjectOnboarding => (
<OnboardingModal
onClose={this.closeOnboarding}
onOpenProjectOnboarding={openProjectOnboarding}
+ skipOnboarding={this.props.skipOnboarding}
/>
)}
</OnboardingContext.Consumer>
@@ -64,7 +50,7 @@ export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps
}
}
-const mapDispatchToProps: DispatchProps = { skipOnboarding };
+const mapDispatchToProps = { skipOnboarding };
export default connect(
null,
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx
new file mode 100644
index 00000000000..a7eecc85f6f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { sortBy } from 'lodash';
+import { Link } from 'react-router';
+import OrganizationsShortListItem from './OrganizationsShortListItem';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export interface Props {
+ organizations: T.Organization[];
+ skipOnboarding: () => void;
+}
+
+export default function OrganizationsShortList({ organizations, skipOnboarding }: Props) {
+ if (organizations.length === 0) {
+ return null;
+ }
+
+ const organizationsShown = sortBy(organizations, organization =>
+ organization.name.toLocaleLowerCase()
+ ).slice(0, 3);
+
+ return (
+ <div>
+ <ul className="account-projects-list">
+ {organizationsShown.map(organization => (
+ <li key={organization.key}>
+ <OrganizationsShortListItem
+ organization={organization}
+ skipOnboarding={skipOnboarding}
+ />
+ </li>
+ ))}
+ </ul>
+ <div className="big-spacer-top">
+ <span className="big-spacer-right">
+ {translateWithParameters('x_of_y_shown', organizationsShown.length, organizations.length)}
+ </span>
+ {organizations.length > 3 && (
+ <Link className="small" onClick={skipOnboarding} to="/account/organizations">
+ {translate('see_all')}
+ </Link>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx
new file mode 100644
index 00000000000..49be2296cb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 OrganizationAvatar from '../../../components/common/OrganizationAvatar';
+import { ListButton } from '../../../components/ui/buttons';
+import { withRouter, Router } from '../../../components/hoc/withRouter';
+import { getOrganizationUrl } from '../../../helpers/urls';
+
+interface Props {
+ organization: T.Organization;
+ router: Router;
+ skipOnboarding: () => void;
+}
+
+export class OrganizationsShortListItem extends React.PureComponent<Props> {
+ handleClick = () => {
+ const { organization, router, skipOnboarding } = this.props;
+ skipOnboarding();
+ router.push(getOrganizationUrl(organization.key));
+ };
+
+ render() {
+ const { organization } = this.props;
+ return (
+ <ListButton className="abs-width-300" onClick={this.handleClick}>
+ <div className="display-flex-center">
+ <OrganizationAvatar className="spacer-right" organization={organization} />
+ <span>{organization.name}</span>
+ </div>
+ </ListButton>
+ );
+ }
+}
+
+export default withRouter(OrganizationsShortListItem);
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
index 7ae1c13dec6..2521fc0cba8 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
@@ -19,31 +19,18 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { OnboardingModal } from '../OnboardingModal';
+import { OnboardingModal, Props } from '../OnboardingModal';
import { click } from '../../../../helpers/testUtils';
+import { mockCurrentUser, mockOrganization } from '../../../../helpers/testMocks';
it('renders correctly', () => {
- expect(
- shallow(
- <OnboardingModal
- currentUser={{ isLoggedIn: true }}
- onClose={jest.fn()}
- onOpenProjectOnboarding={jest.fn()}
- />
- )
- ).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
-it('should correctly open the different tutorials', () => {
+it('should open project create page', () => {
const onClose = jest.fn();
const onOpenProjectOnboarding = jest.fn();
- const wrapper = shallow(
- <OnboardingModal
- currentUser={{ isLoggedIn: true }}
- onClose={onClose}
- onOpenProjectOnboarding={onOpenProjectOnboarding}
- />
- );
+ const wrapper = shallowRender({ onClose, onOpenProjectOnboarding });
click(wrapper.find('ResetButtonLink'));
expect(onClose).toHaveBeenCalled();
@@ -51,3 +38,30 @@ it('should correctly open the different tutorials', () => {
wrapper.find('Button').forEach(button => click(button));
expect(onOpenProjectOnboarding).toHaveBeenCalled();
});
+
+it('should display organization list if any', () => {
+ const wrapper = shallowRender({
+ currentUser: mockCurrentUser({ personalOrganization: 'personal' }),
+ userOrganizations: [
+ mockOrganization({ key: 'a', name: 'Arthur' }),
+ mockOrganization({ key: 'd', name: 'Daniel Inc' }),
+ mockOrganization({ key: 'personal', name: 'Personal' })
+ ]
+ });
+
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('OrganizationsShortList').prop('organizations')).toHaveLength(2);
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <OnboardingModal
+ currentUser={mockCurrentUser()}
+ onClose={jest.fn()}
+ onOpenProjectOnboarding={jest.fn()}
+ skipOnboarding={jest.fn()}
+ userOrganizations={[]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx
new file mode 100644
index 00000000000..f0e9b24e4d3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 OrganizationsShortList, { Props } from '../OrganizationsShortList';
+import { mockOrganization } from '../../../../helpers/testMocks';
+
+it('should render null with no orgs', () => {
+ expect(shallowRender().getElement()).toBe(null);
+});
+
+it('should render correctly', () => {
+ const wrapper = shallowRender({
+ organizations: [mockOrganization(), mockOrganization({ key: 'bar', name: 'Bar' })]
+ });
+
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('li')).toHaveLength(2);
+});
+
+it('should limit displayed orgs to the first three', () => {
+ const wrapper = shallowRender({
+ organizations: [
+ mockOrganization(),
+ mockOrganization({ key: 'zoo', name: 'Zoological' }),
+ mockOrganization({ key: 'bar', name: 'Bar' }),
+ mockOrganization({ key: 'kor', name: 'Kor' })
+ ]
+ });
+
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('li')).toHaveLength(3);
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <OrganizationsShortList organizations={[]} skipOnboarding={jest.fn()} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx
new file mode 100644
index 00000000000..bf84c7895ba
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx
@@ -0,0 +1,50 @@
+/*
+ * 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 { OrganizationsShortListItem } from '../OrganizationsShortListItem';
+import { mockRouter, mockOrganization } from '../../../../helpers/testMocks';
+import { click } from '../../../../helpers/testUtils';
+
+it('renders correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('calls skiponboarding and redirects to org page', () => {
+ const skipOnboarding = jest.fn();
+ const push = jest.fn();
+ const wrapper = shallowRender({ skipOnboarding, router: mockRouter({ push }) });
+
+ click(wrapper);
+
+ expect(skipOnboarding).toHaveBeenCalledTimes(1);
+ expect(push).toHaveBeenCalledWith('/organizations/foo');
+});
+
+function shallowRender(props: Partial<OrganizationsShortListItem['props']> = {}) {
+ return shallow(
+ <OrganizationsShortListItem
+ organization={mockOrganization()}
+ router={mockRouter()}
+ skipOnboarding={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
index daf332ad6a2..1c9eb96f30e 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
@@ -20,21 +20,111 @@ exports[`renders correctly 1`] = `
</p>
</div>
<div
- className="modal-body text-center huge-spacer-top huge-spacer-bottom"
+ className="modal-body text-center display-flex-row huge-spacer-top huge-spacer-bottom"
>
- <OnboardingProjectIcon
- className="big-spacer-bottom"
- />
- <h6
- className="onboarding-choice-name big-spacer-bottom"
- >
- onboarding.analyze_your_code
- </h6>
- <Button
+ <div
+ className="flex-1"
+ >
+ <OnboardingProjectIcon
+ className="big-spacer-bottom"
+ />
+ <h6
+ className="onboarding-choice-name big-spacer-bottom"
+ >
+ onboarding.analyze_your_code
+ </h6>
+ <Button
+ onClick={[MockFunction]}
+ >
+ onboarding.project.create
+ </Button>
+ </div>
+ </div>
+ <div
+ className="modal-foot text-right"
+ >
+ <ResetButtonLink
onClick={[MockFunction]}
>
- onboarding.project.create
- </Button>
+ not_now
+ </ResetButtonLink>
+ </div>
+</Modal>
+`;
+
+exports[`should display organization list if any 1`] = `
+<Modal
+ contentLabel="onboarding.header"
+ medium={true}
+ onRequestClose={[MockFunction]}
+ shouldCloseOnOverlayClick={false}
+>
+ <div
+ className="modal-head"
+ >
+ <h2>
+ onboarding.header
+ </h2>
+ <p
+ className="spacer-top"
+ >
+ onboarding.header.description
+ </p>
+ </div>
+ <div
+ className="modal-body text-center display-flex-row huge-spacer-top huge-spacer-bottom"
+ >
+ <div
+ className="flex-1"
+ >
+ <OnboardingProjectIcon
+ className="big-spacer-bottom"
+ />
+ <h6
+ className="onboarding-choice-name big-spacer-bottom"
+ >
+ onboarding.analyze_your_code
+ </h6>
+ <Button
+ onClick={[MockFunction]}
+ >
+ onboarding.project.create
+ </Button>
+ </div>
+ <div
+ className="vertical-pipe-separator"
+ >
+ <div
+ className="vertical-separator"
+ />
+ </div>
+ <div
+ className="flex-1"
+ >
+ <OnboardingTeamIcon
+ className="big-spacer-bottom"
+ />
+ <h6
+ className="onboarding-choice-name big-spacer-bottom"
+ >
+ onboarding.browse_your_organizations
+ </h6>
+ <OrganizationsShortList
+ organizations={
+ Array [
+ Object {
+ "key": "a",
+ "name": "Arthur",
+ },
+ Object {
+ "key": "d",
+ "name": "Daniel Inc",
+ },
+ ]
+ }
+ skipOnboarding={[MockFunction]}
+ />
+ </div>
</div>
<div
className="modal-foot text-right"
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap
new file mode 100644
index 00000000000..5410482f395
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap
@@ -0,0 +1,111 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should limit displayed orgs to the first three 1`] = `
+<div>
+ <ul
+ className="account-projects-list"
+ >
+ <li
+ key="bar"
+ >
+ <withRouter(OrganizationsShortListItem)
+ organization={
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ }
+ }
+ skipOnboarding={[MockFunction]}
+ />
+ </li>
+ <li
+ key="foo"
+ >
+ <withRouter(OrganizationsShortListItem)
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ skipOnboarding={[MockFunction]}
+ />
+ </li>
+ <li
+ key="kor"
+ >
+ <withRouter(OrganizationsShortListItem)
+ organization={
+ Object {
+ "key": "kor",
+ "name": "Kor",
+ }
+ }
+ skipOnboarding={[MockFunction]}
+ />
+ </li>
+ </ul>
+ <div
+ className="big-spacer-top"
+ >
+ <span
+ className="big-spacer-right"
+ >
+ x_of_y_shown.3.4
+ </span>
+ <Link
+ className="small"
+ onClick={[MockFunction]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account/organizations"
+ >
+ see_all
+ </Link>
+ </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div>
+ <ul
+ className="account-projects-list"
+ >
+ <li
+ key="bar"
+ >
+ <withRouter(OrganizationsShortListItem)
+ organization={
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ }
+ }
+ skipOnboarding={[MockFunction]}
+ />
+ </li>
+ <li
+ key="foo"
+ >
+ <withRouter(OrganizationsShortListItem)
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ skipOnboarding={[MockFunction]}
+ />
+ </li>
+ </ul>
+ <div
+ className="big-spacer-top"
+ >
+ <span
+ className="big-spacer-right"
+ >
+ x_of_y_shown.2.2
+ </span>
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap
new file mode 100644
index 00000000000..ab088a9991b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<ListButton
+ className="abs-width-300"
+ onClick={[Function]}
+>
+ <div
+ className="display-flex-center"
+ >
+ <OrganizationAvatar
+ className="spacer-right"
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <span>
+ Foo
+ </span>
+ </div>
+</ListButton>
+`;
diff --git a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
index 2fd85afbfbb..c9b44d0f7bd 100644
--- a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
+++ b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
@@ -23,7 +23,7 @@ import { withCurrentUser } from './withCurrentUser';
import { isLoggedIn } from '../../helpers/users';
import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
-export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
+export function whenLoggedIn<P>(WrappedComponent: React.ComponentType<P>) {
class Wrapper extends React.Component<P & { currentUser: T.CurrentUser }> {
static displayName = getWrappedDisplayName(WrappedComponent, 'whenLoggedIn');
diff --git a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx
index 991005055e2..c016ce0ffa5 100644
--- a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx
+++ b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx
@@ -29,7 +29,7 @@ interface OwnProps {
}
export function withUserOrganizations<P>(
- WrappedComponent: React.ComponentClass<P & Partial<OwnProps>>
+ WrappedComponent: React.ComponentType<P & Partial<OwnProps>>
) {
class Wrapper extends React.Component<P & OwnProps> {
static displayName = getWrappedDisplayName(WrappedComponent, 'withUserOrganizations');
diff --git a/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
new file mode 100644
index 00000000000..ebedfeb2745
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 Icon, { IconProps } from './Icon';
+import * as theme from '../../app/theme';
+
+export default function OnboardingTeamIcon({ className, fill = theme.darkBlue, size }: IconProps) {
+ return (
+ <Icon className={className} size={size || 64} viewBox="0 0 64 64">
+ <g fill="none" fillRule="evenodd" stroke={fill} strokeWidth="2">
+ <path d="M32 9v5M11.5195 43.0898l7.48-4.091m33.481-18.0994l-7.48 4.1m-33.481-4.1l7.48 4.1M45 38.999l7.48 4.101M32 50v5m15-23c0 8.284-6.715 15-15 15s-15-6.716-15-15c0-8.285 6.715-15 15-15s15 6.715 15 15z" />
+ <path d="M40 38c0 1.656-3.58 2-8 2s-8-.344-8-2m16 0v-3l-5-3-1-1m-10 7v-3l5-3 1-1m6-4c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm-.0098-21.71c7.18 1.069 13.439 4.96 17.609 10.51m-17.609 42.91c7.18-1.07 13.439-4.96 17.609-10.51M6.6299 41.25c-1.06-2.88-1.63-6-1.63-9.25s.57-6.37 1.63-9.25m3.7705-6.9502c4.17-5.55 10.43-9.44 17.609-10.51m-17.609 42.9104c4.17 5.55 10.43 9.439 17.609 10.51M57.3701 22.75c1.06 2.88 1.63 6 1.63 9.25s-.57 6.37-1.63 9.25" />
+ <path d="M36 5c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 19c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 45c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M36 59c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2" />
+ </g>
+ </Icon>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/ui/buttons.css b/server/sonar-web/src/main/js/components/ui/buttons.css
index 05df1afe181..cede4ec4d10 100644
--- a/server/sonar-web/src/main/js/components/ui/buttons.css
+++ b/server/sonar-web/src/main/js/components/ui/buttons.css
@@ -79,6 +79,7 @@
.button-red:focus {
box-shadow: 0 0 0 3px rgba(212, 51, 63, 0.25);
}
+
/* #endregion */
/* #region .button-success */
@@ -96,6 +97,7 @@
.button-success:focus {
box-shadow: 0 0 0 3px rgba(0, 170, 0, 0.25);
}
+
/* #endregion */
/* #region .button-grey */
@@ -113,12 +115,14 @@
.button-grey:focus {
box-shadow: 0 0 0 3px rgba(180, 180, 180, 0.25);
}
+
/* #endregion */
/* #region .button-link */
.button-link {
display: inline-flex;
- height: auto; /* Keep this to not inherit the height from .button */
+ height: auto;
+ /* Keep this to not inherit the height from .button */
line-height: 1;
margin: 0;
padding: 0;
@@ -154,6 +158,7 @@
background: transparent !important;
cursor: default;
}
+
/* #endregion */
.button-small {
@@ -217,6 +222,7 @@
margin: 0 8px;
font-size: var(--smallFontSize);
}
+
/* #endregion */
/* #region .button-icon */
@@ -261,4 +267,23 @@
.button-icon:focus svg {
color: #fff;
}
+
/* #endregion */
+
+.button-list {
+ height: auto;
+ border: 1px solid var(--barBorderColor);
+ padding: var(--gridSize);
+ margin: calc(var(--gridSize) / 2);
+ text-align: left;
+ justify-content: space-between;
+ color: var(--secondFontColor);
+ font-weight: normal;
+}
+
+.button-list:hover {
+ background-color: white;
+ border-color: var(--blue);
+ color: var(--darkBlue);
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.175);
+}
diff --git a/server/sonar-web/src/main/js/components/ui/buttons.tsx b/server/sonar-web/src/main/js/components/ui/buttons.tsx
index a0dd58c1455..99e4c327a62 100644
--- a/server/sonar-web/src/main/js/components/ui/buttons.tsx
+++ b/server/sonar-web/src/main/js/components/ui/buttons.tsx
@@ -24,6 +24,7 @@ import ClearIcon from '../icons-components/ClearIcon';
import EditIcon from '../icons-components/EditIcon';
import Tooltip from '../controls/Tooltip';
import './buttons.css';
+import ChevronRightIcon from '../icons-components/ChevronRightcon';
type AllowedButtonAttributes = Pick<
React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -138,3 +139,12 @@ export function EditButton(props: ActionButtonProps) {
</ButtonIcon>
);
}
+
+export function ListButton({ className, children, ...props }: ButtonProps) {
+ return (
+ <Button className={classNames('button-list', className)} {...props}>
+ {children}
+ <ChevronRightIcon />
+ </Button>
+ );
+}