aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-07-20 16:57:23 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-10 20:21:28 +0200
commitb08814f7807c1443592af65cd68c2a51dfd4ee37 (patch)
treed7bbaf30c5c0633cd212a30e52db073945ba61ea /server/sonar-web/src
parent3a39b4fa08b15912c928af35fb7b77cd4b85ab64 (diff)
downloadsonarqube-b08814f7807c1443592af65cd68c2a51dfd4ee37.tar.gz
sonarqube-b08814f7807c1443592af65cd68c2a51dfd4ee37.zip
SONAR-11036 Install integration with GitHub or BitBucket Cloud
* SONAR-11040 Update tutorial choices modal * SONAR-11041 Migrate manual installation tab * SONAR-11041 Rename button to start new project tutorial * SONAR-11041 Rework sonarcloud tabbed page styling * SONAR-11042 Add alm app install buttons in create project page * Make start script compatible with ALM integration
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/alm-integration.ts (renamed from server/sonar-web/src/main/js/apps/projectsManagement/utils.ts)16
-rw-r--r--server/sonar-web/src/main/js/api/components.ts29
-rw-r--r--server/sonar-web/src/main/js/app/components/StartupModal.tsx16
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/alerts.css13
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/modals.css18
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/forms.css12
-rw-r--r--server/sonar-web/src/main/js/app/styles/sonarcloud.css67
-rw-r--r--server/sonar-web/src/main/js/app/theme.js1
-rw-r--r--server/sonar-web/src/main/js/app/types.ts2
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js8
-rw-r--r--server/sonar-web/src/main/js/apps/about/routes.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/App.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx8
-rwxr-xr-xserver/sonar-web/src/main/js/apps/securityReports/components/App.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap245
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css16
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css35
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap55
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap116
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx132
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx182
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx227
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap39
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap97
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap125
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/routes.ts36
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/styles.css40
-rw-r--r--server/sonar-web/src/main/js/components/controls/react-select.css1
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/OnboardingPrivateIcon.tsx50
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx37
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx33
-rw-r--r--server/sonar-web/src/main/js/components/ui/IdentityProviderLink.css68
-rw-r--r--server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx62
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/IdentityProviderLink-test.tsx39
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap21
50 files changed, 1868 insertions, 403 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts b/server/sonar-web/src/main/js/api/alm-integration.ts
index 183b47bc732..0232632ccc2 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
+++ b/server/sonar-web/src/main/js/api/alm-integration.ts
@@ -17,10 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { SearchProjectsResponseComponent } from '../../api/components';
+import { getJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
-export const PAGE_SIZE = 50;
-
-export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];
-
-export type Project = SearchProjectsResponseComponent;
+export function getRepositories(): Promise<{
+ installation: {
+ installationUrl: string;
+ enabled: boolean;
+ };
+}> {
+ return getJSON('/api/alm_integration/list_repositories').catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index a069c38b17e..c917f8a4460 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -17,8 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON, postJSON, post, RequestData } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
+import { getJSON, postJSON, post, RequestData } from '../helpers/request';
import { Paging, Visibility, BranchParameters, MyProject } from '../app/types';
export interface BaseSearchProjectsParameters {
@@ -31,29 +31,30 @@ export interface BaseSearchProjectsParameters {
visibility?: Visibility;
}
-export interface SearchProjectsParameters extends BaseSearchProjectsParameters {
- p?: number;
- ps?: number;
+export interface ProjectBase {
+ key: string;
+ name: string;
+ qualifier: string;
+ visibility: Visibility;
}
-export interface SearchProjectsResponseComponent {
+export interface Project extends ProjectBase {
id: string;
- key: string;
lastAnalysisDate?: string;
- name: string;
organization: string;
- qualifier: string;
- visibility: Visibility;
}
-export interface SearchProjectsResponse {
- components: SearchProjectsResponseComponent[];
- paging: Paging;
+export interface SearchProjectsParameters extends BaseSearchProjectsParameters {
+ p?: number;
+ ps?: number;
}
export function getComponents(
parameters: SearchProjectsParameters
-): Promise<SearchProjectsResponse> {
+): Promise<{
+ components: Project[];
+ paging: Paging;
+}> {
return getJSON('/api/projects/search', parameters);
}
@@ -75,7 +76,7 @@ export function createProject(data: {
name: string;
project: string;
organization?: string;
-}): Promise<any> {
+}): Promise<{ project: ProjectBase }> {
return postJSON('/api/projects/create', data).catch(throwGlobalError);
}
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 bbd6c574eec..c49a77d2052 100644
--- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx
+++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
@@ -99,11 +99,13 @@ export class StartupModal extends React.PureComponent<Props, State> {
this.tryAutoOpenLicense().catch(this.tryAutoOpenOnboarding);
}
- closeOnboarding = () => {
+ closeOnboarding = (doSkipOnboarding = true) => {
this.setState(state => {
if (state.modal !== ModalKey.license) {
- skipOnboarding();
- this.props.skipOnboardingAction();
+ if (doSkipOnboarding) {
+ skipOnboarding();
+ this.props.skipOnboardingAction();
+ }
return { automatic: false, modal: undefined };
}
return undefined;
@@ -164,7 +166,10 @@ export class StartupModal extends React.PureComponent<Props, State> {
tryAutoOpenOnboarding = () => {
const { currentUser, location } = this.props;
- if (currentUser.showOnboardingTutorial && !location.pathname.startsWith('documentation')) {
+ if (
+ currentUser.showOnboardingTutorial &&
+ !['about', 'documentation', 'onboarding'].some(path => location.pathname.startsWith(path))
+ ) {
this.setState({ automatic: true });
if (isSonarCloud()) {
this.openOnboarding();
@@ -182,9 +187,8 @@ export class StartupModal extends React.PureComponent<Props, State> {
{modal === ModalKey.license && <LicensePromptModal onClose={this.closeLicense} />}
{modal === ModalKey.onboarding && (
<Onboarding
- onFinish={this.closeOnboarding}
+ onClose={this.closeOnboarding}
onOpenOrganizationOnboarding={this.openOrganizationOnboarding}
- onOpenProjectOnboarding={this.openProjectOnboarding}
onOpenTeamOnboarding={this.openTeamOnboarding}
/>
)}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
index 412846db466..58030362c11 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
@@ -69,7 +69,7 @@ export default class GlobalNavPlus extends React.PureComponent<Props, State> {
<ul className="menu">
<li>
<a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
- {translate('my_account.analyze_new_project')}
+ {translate('provisioning.create_new_project')}
</a>
</li>
<li className="divider" />
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
index 1664c15d58c..56a23c2dc5e 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
@@ -12,7 +12,7 @@ exports[`render 1`] = `
href="#"
onClick={[Function]}
>
- my_account.analyze_new_project
+ provisioning.create_new_project
</a>
</li>
<li
diff --git a/server/sonar-web/src/main/js/app/styles/components/alerts.css b/server/sonar-web/src/main/js/app/styles/components/alerts.css
index ce827c16d03..b965dced2d6 100644
--- a/server/sonar-web/src/main/js/app/styles/components/alerts.css
+++ b/server/sonar-web/src/main/js/app/styles/components/alerts.css
@@ -60,14 +60,21 @@
color: #3c763d;
}
+.alert-muted {
+ color: var(--disableGrayText);
+ border-color: var(--disableGrayBorder);
+ background-color: var(--disableGrayBg);
+}
+
.alert-big {
font-size: var(--mediumFontSize);
padding: 10px 16px;
}
-.page-header .alert {
- clear: left;
- float: left;
+.alert-small {
+ font-size: var(--verySmallFontSize);
+ margin-bottom: 0;
+ padding: 2px 4px;
}
.page-notifs .alert {
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 1e958e4b526..98755726661 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
@@ -101,6 +101,24 @@
padding: 10px;
}
+.modal-simple-head {
+ padding: calc(2 * var(--pagePadding)) calc(3 * var(--pagePadding));
+}
+
+.modal-simple-head h1 {
+ font-size: var(--hugeFontSize);
+ font-weight: bold;
+ line-height: 30px;
+}
+
+.modal-simple-body {
+ padding: 0 calc(3 * var(--pagePadding));
+}
+
+.modal-simple-footer {
+ padding: calc(2 * var(--pagePadding)) calc(3 * var(--pagePadding));
+}
+
.modal-field,
.modal-large-field,
.modal-validation-field {
diff --git a/server/sonar-web/src/main/js/app/styles/init/forms.css b/server/sonar-web/src/main/js/app/styles/init/forms.css
index 881e78fe804..93a7d5c7a15 100644
--- a/server/sonar-web/src/main/js/app/styles/init/forms.css
+++ b/server/sonar-web/src/main/js/app/styles/init/forms.css
@@ -172,6 +172,18 @@ label[for] {
cursor: pointer;
}
+.form-field {
+ clear: both;
+ display: block;
+ padding-top: var(--gridSize);
+ padding-bottom: calc(2 * var(--gridSize));
+}
+
+.form-field label {
+ display: block;
+ padding-bottom: var(--gridSize);
+}
+
.radio-toggle {
display: inline-block;
vertical-align: middle;
diff --git a/server/sonar-web/src/main/js/app/styles/sonarcloud.css b/server/sonar-web/src/main/js/app/styles/sonarcloud.css
new file mode 100644
index 00000000000..66a4f2f0ba8
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/styles/sonarcloud.css
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/* EXTENDS components/pages.css */
+.sonarcloud.page-limited {
+ padding-top: 50px;
+ padding-bottom: 50px;
+}
+
+.sonarcloud .page-header {
+ margin-bottom: 40px;
+}
+
+.sonarcloud .page-title {
+ font-size: var(--hugeFontSize);
+ font-weight: bold;
+}
+
+.sonarcloud .flex-tabs {
+ display: flex;
+ clear: left;
+ margin-bottom: calc(3 * var(--gridSize));
+ box-shadow: 0 1px 0 var(--barBorderColor);
+}
+
+.sonarcloud .flex-tabs > li > a {
+ display: block;
+ height: 100%;
+ width: 100%;
+ box-sizing: border-box;
+ color: var(--secondFontColor);
+ font-weight: 600;
+ cursor: pointer;
+ padding-bottom: var(--gridSize);
+ border-bottom: 2px solid transparent;
+ transition: color 0.2s ease;
+}
+
+.sonarcloud .flex-tabs > li ~ li {
+ margin-left: calc(4 * var(--gridSize));
+}
+
+.sonarcloud .flex-tabs > li > a:hover {
+ color: var(--baseFontColor);
+}
+
+.sonarcloud .flex-tabs > li > a.selected {
+ color: var(--blue);
+ border-bottom-color: var(--blue);
+}
diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js
index 91ba51aa4c3..0a90beef99b 100644
--- a/server/sonar-web/src/main/js/app/theme.js
+++ b/server/sonar-web/src/main/js/app/theme.js
@@ -64,6 +64,7 @@ module.exports = {
smallFontSize: '12px',
mediumFontSize: '14px',
bigFontSize: '16px',
+ hugeFontSize: '24px',
controlHeight: `${3 * grid}px`,
smallControlHeight: `${2.5 * grid}px`,
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index e02ced80d7e..4f1b00c3f76 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -305,6 +305,8 @@ export interface LinearIssueLocation {
export interface LoggedInUser extends CurrentUser {
avatar?: string;
email?: string;
+ externalIdentity?: string;
+ externalProvider?: string;
homepage?: HomePage;
isLoggedIn: true;
login: string;
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index 69602ff1e13..788cd86620a 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -45,6 +45,7 @@ import IssuesPageSelector from '../../apps/issues/IssuesPageSelector';
import marketplaceRoutes from '../../apps/marketplace/routes';
import customMetricsRoutes from '../../apps/custom-metrics/routes';
import overviewRoutes from '../../apps/overview/routes';
+import onboardingRoutes from '../../apps/tutorials/routes';
import organizationsRoutes from '../../apps/organizations/routes';
import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import portfolioRoutes from '../../apps/portfolio/routes';
@@ -169,12 +170,7 @@ const startReactApp = (lang, currentUser, appState) => {
component={lazyLoad(() => import('../components/extensions/GlobalPageExtension'))}
/>
<Route path="issues" component={IssuesPageSelector} />
- <Route
- path="onboarding"
- component={lazyLoad(() =>
- import('../../apps/tutorials/projectOnboarding/ProjectOnboardingPage')
- )}
- />
+ <Route path="onboarding" childRoutes={onboardingRoutes} />
<Route path="organizations" childRoutes={organizationsRoutes} />
<Route path="projects" childRoutes={projectsRoutes} />
<Route path="quality_gates" childRoutes={qualityGatesRoutes} />
diff --git a/server/sonar-web/src/main/js/apps/about/routes.ts b/server/sonar-web/src/main/js/apps/about/routes.ts
index 315b604da61..c41f05acfc2 100644
--- a/server/sonar-web/src/main/js/apps/about/routes.ts
+++ b/server/sonar-web/src/main/js/apps/about/routes.ts
@@ -27,7 +27,7 @@ const routes = [
() => (isSonarCloud() ? import('./sonarcloud/Home') : import('./components/AboutApp'))
)
},
- childRoutes: isSonarCloud
+ childRoutes: isSonarCloud()
? [
{
path: 'contact',
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
index 89e595feed4..2148e50098f 100644
--- a/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
+++ b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
@@ -34,7 +34,7 @@ export default class UserExternalIdentity extends React.PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.user !== this.props.user) {
- this.this.fetchIdentityProviders();
+ this.fetchIdentityProviders();
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
index 4cd7b7d2164..c5727397933 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
@@ -55,7 +55,9 @@ export class NoFavoriteProjects extends React.PureComponent<StateProps> {
<p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p>
<div className="huge-spacer-top">
<a className="button" href="#" onClick={this.onAnalyzeProjectClick}>
- {translate('my_account.analyze_new_project')}
+ {isSonarCloud()
+ ? translate('provisioning.create_new_project')
+ : translate('my_account.analyze_new_project')}
</a>
<Dropdown
className="display-inline-block big-spacer-left"
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
index 8cf6f203496..a73ab9e5044 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
@@ -50,7 +50,7 @@ exports[`renders for SonarCloud 1`] = `
href="#"
onClick={[Function]}
>
- my_account.analyze_new_project
+ provisioning.create_new_project
</a>
<Dropdown
className="display-inline-block big-spacer-left"
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
index 2a5bdbeee27..4df65fc78df 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
@@ -24,10 +24,9 @@ import Header from './Header';
import Search from './Search';
import Projects from './Projects';
import CreateProjectForm from './CreateProjectForm';
-import { PAGE_SIZE, Project } from './utils';
import ListFooter from '../../components/controls/ListFooter';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
-import { getComponents } from '../../api/components';
+import { getComponents, Project } from '../../api/components';
import { Organization, Visibility } from '../../app/types';
import { toNotSoISOString } from '../../helpers/dates';
import { translate } from '../../helpers/l10n';
@@ -54,6 +53,8 @@ interface State {
visibility?: Visibility;
}
+const PAGE_SIZE = 50;
+
export default class App extends React.PureComponent<Props, State> {
mounted = false;
@@ -94,19 +95,22 @@ export default class App extends React.PureComponent<Props, State> {
qualifiers: this.state.qualifiers,
visibility: this.state.visibility
};
- getComponents(parameters).then(r => {
- if (this.mounted) {
- let projects: Project[] = r.components;
- if (this.state.page > 1) {
- projects = [...this.state.projects, ...projects];
+ getComponents(parameters).then(
+ r => {
+ if (this.mounted) {
+ let projects: Project[] = r.components;
+ if (this.state.page > 1) {
+ projects = [...this.state.projects, ...projects];
+ }
+ this.setState({ ready: true, projects, selection: [], total: r.paging.total });
}
- this.setState({ ready: true, projects, selection: [], total: r.paging.total });
- }
- });
+ },
+ () => {}
+ );
};
loadMore = () => {
- this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects);
+ this.setState(({ page }) => ({ ready: false, page: page + 1 }), this.requestProjects);
};
onSearch = (query: string) => {
@@ -152,18 +156,15 @@ export default class App extends React.PureComponent<Props, State> {
this.setState({ ready: false, page: 1, analyzedBefore }, this.requestProjects);
onProjectSelected = (project: string) => {
- const newSelection = uniq([...this.state.selection, project]);
- this.setState({ selection: newSelection });
+ this.setState(({ selection }) => ({ selection: uniq([...selection, project]) }));
};
onProjectDeselected = (project: string) => {
- const newSelection = without(this.state.selection, project);
- this.setState({ selection: newSelection });
+ this.setState(({ selection }) => ({ selection: without(selection, project) }));
};
onAllSelected = () => {
- const newSelection = this.state.projects.map(project => project.key);
- this.setState({ selection: newSelection });
+ this.setState(({ projects }) => ({ selection: projects.map(project => project.key) }));
};
onAllDeselected = () => {
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
index 8853af4a079..7f1b12c3e4d 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
@@ -20,11 +20,11 @@
import * as React from 'react';
import { Link } from 'react-router';
import ProjectRowActions from './ProjectRowActions';
-import { Project } from './utils';
import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer';
import Checkbox from '../../components/controls/Checkbox';
import QualifierIcon from '../../components/icons-components/QualifierIcon';
import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
+import { Project } from '../../api/components';
interface Props {
currentUser: { login: string };
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
index 438db5c8a77..5932a17e570 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
@@ -19,9 +19,8 @@
*/
import * as React from 'react';
import RestoreAccessModal from './RestoreAccessModal';
-import { Project } from './utils';
import ApplyTemplate from '../permissions/project/components/ApplyTemplate';
-import { getComponentShow } from '../../api/components';
+import { getComponentShow, Project } from '../../api/components';
import { getComponentNavigation } from '../../api/nav';
import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown';
import { translate } from '../../helpers/l10n';
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
index 7e72fb9b057..09add750d94 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
@@ -20,9 +20,9 @@
import * as React from 'react';
import * as classNames from 'classnames';
import ProjectRow from './ProjectRow';
-import { Project } from './utils';
import { Organization } from '../../app/types';
import { translate } from '../../helpers/l10n';
+import { Project } from '../../api/components';
interface Props {
currentUser: { login: string };
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
index 57183554451..7c402913a25 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
@@ -19,11 +19,11 @@
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { Project } from './utils';
import { grantPermissionToUser } from '../../api/permissions';
import Modal from '../../components/controls/Modal';
import { SubmitButton, ResetButtonLink } from '../../components/ui/buttons';
import { translate } from '../../helpers/l10n';
+import { Project } from '../../api/components';
interface Props {
currentUser: { login: string };
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
index 205526b58aa..f336153d976 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
@@ -21,16 +21,16 @@ import * as React from 'react';
import { sortBy } from 'lodash';
import BulkApplyTemplateModal from './BulkApplyTemplateModal';
import DeleteModal from './DeleteModal';
-import { QUALIFIERS_ORDER, Project } from './utils';
-import { Organization, Visibility } from '../../app/types';
import Checkbox from '../../components/controls/Checkbox';
-import { translate } from '../../helpers/l10n';
import QualifierIcon from '../../components/icons-components/QualifierIcon';
import HelpTooltip from '../../components/controls/HelpTooltip';
import DateInput from '../../components/controls/DateInput';
import Select from '../../components/controls/Select';
import SearchBox from '../../components/controls/SearchBox';
import { Button } from '../../components/ui/buttons';
+import { Project } from '../../api/components';
+import { Organization, Visibility } from '../../app/types';
+import { translate } from '../../helpers/l10n';
export interface Props {
analyzedBefore: Date | undefined;
@@ -59,6 +59,8 @@ interface State {
deleteModal: boolean;
}
+const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];
+
export default class Search extends React.PureComponent<Props, State> {
mounted = false;
state: State = { bulkApplyTemplateModal: false, deleteModal: false };
diff --git a/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx b/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
index fa966581a94..def1019b4b4 100755
--- a/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
@@ -34,8 +34,8 @@ import { getSecurityHotspots } from '../../../api/security-reports';
import { isLongLivingBranch } from '../../../helpers/branches';
import DocTooltip from '../../../components/docs/DocTooltip';
import { getRulesUrl } from '../../../helpers/urls';
-import '../style.css';
import { isSonarCloud } from '../../../helpers/system';
+import '../style.css';
interface Props {
branchLike?: BranchLike;
@@ -145,23 +145,24 @@ export default class App extends React.PureComponent<Props, State> {
to={{ pathname: '/documentation/security-reports' }}>
{translate('learn_more')}
</Link>
- </div>
- <div className="alert alert-info spacer-top">
- <FormattedMessage
- defaultMessage={translate('security_reports.info')}
- id="security_reports.info"
- values={{
- link: (
- <Link
- to={getRulesUrl(
- { types: 'SECURITY_HOTSPOT,VULNERABILITY' },
- isSonarCloud() ? component.organization : undefined
- )}>
- {translate('security_reports.info.link')}
- </Link>
- )
- }}
- />
+ <p className="alert alert-info spacer-top display-inline-block">
+ <FormattedMessage
+ defaultMessage={translate('security_reports.info')}
+ id="security_reports.info"
+ tagName="p"
+ values={{
+ link: (
+ <Link
+ to={getRulesUrl(
+ { types: 'SECURITY_HOTSPOT,VULNERABILITY' },
+ isSonarCloud() ? component.organization : undefined
+ )}>
+ {translate('security_reports.info.link')}
+ </Link>
+ )
+ }}
+ />
+ </p>
</div>
</header>
<div className="display-inline-flex-center">
diff --git a/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
index b787a7c99a8..86b8d8c2b57 100644
--- a/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -38,32 +38,33 @@ exports[`handle checkbox for cwe display 1`] = `
>
learn_more
</Link>
- </div>
- <div
- className="alert alert-info spacer-top"
- >
- <FormattedMessage
- defaultMessage="security_reports.info"
- id="security_reports.info"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/coding_rules",
- "query": Object {
- "types": "SECURITY_HOTSPOT,VULNERABILITY",
- },
+ <p
+ className="alert alert-info spacer-top display-inline-block"
+ >
+ <FormattedMessage
+ defaultMessage="security_reports.info"
+ id="security_reports.info"
+ tagName="p"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "types": "SECURITY_HOTSPOT,VULNERABILITY",
+ },
+ }
}
- }
- >
- security_reports.info.link
- </Link>,
+ >
+ security_reports.info.link
+ </Link>,
+ }
}
- }
- />
+ />
+ </p>
</div>
</header>
<div
@@ -147,32 +148,33 @@ exports[`handle checkbox for cwe display 2`] = `
>
learn_more
</Link>
- </div>
- <div
- className="alert alert-info spacer-top"
- >
- <FormattedMessage
- defaultMessage="security_reports.info"
- id="security_reports.info"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/coding_rules",
- "query": Object {
- "types": "SECURITY_HOTSPOT,VULNERABILITY",
- },
+ <p
+ className="alert alert-info spacer-top display-inline-block"
+ >
+ <FormattedMessage
+ defaultMessage="security_reports.info"
+ id="security_reports.info"
+ tagName="p"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "types": "SECURITY_HOTSPOT,VULNERABILITY",
+ },
+ }
}
- }
- >
- security_reports.info.link
- </Link>,
+ >
+ security_reports.info.link
+ </Link>,
+ }
}
- }
- />
+ />
+ </p>
</div>
</header>
<div
@@ -299,32 +301,33 @@ exports[`renders owaspTop10 1`] = `
>
learn_more
</Link>
- </div>
- <div
- className="alert alert-info spacer-top"
- >
- <FormattedMessage
- defaultMessage="security_reports.info"
- id="security_reports.info"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/coding_rules",
- "query": Object {
- "types": "SECURITY_HOTSPOT,VULNERABILITY",
- },
+ <p
+ className="alert alert-info spacer-top display-inline-block"
+ >
+ <FormattedMessage
+ defaultMessage="security_reports.info"
+ id="security_reports.info"
+ tagName="p"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "types": "SECURITY_HOTSPOT,VULNERABILITY",
+ },
+ }
}
- }
- >
- security_reports.info.link
- </Link>,
+ >
+ security_reports.info.link
+ </Link>,
+ }
}
- }
- />
+ />
+ </p>
</div>
</header>
<div
@@ -408,32 +411,33 @@ exports[`renders sansTop25 1`] = `
>
learn_more
</Link>
- </div>
- <div
- className="alert alert-info spacer-top"
- >
- <FormattedMessage
- defaultMessage="security_reports.info"
- id="security_reports.info"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/coding_rules",
- "query": Object {
- "types": "SECURITY_HOTSPOT,VULNERABILITY",
- },
+ <p
+ className="alert alert-info spacer-top display-inline-block"
+ >
+ <FormattedMessage
+ defaultMessage="security_reports.info"
+ id="security_reports.info"
+ tagName="p"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "types": "SECURITY_HOTSPOT,VULNERABILITY",
+ },
+ }
}
- }
- >
- security_reports.info.link
- </Link>,
+ >
+ security_reports.info.link
+ </Link>,
+ }
}
- }
- />
+ />
+ </p>
</div>
</header>
<div
@@ -517,32 +521,33 @@ exports[`renders with cwe 1`] = `
>
learn_more
</Link>
- </div>
- <div
- className="alert alert-info spacer-top"
- >
- <FormattedMessage
- defaultMessage="security_reports.info"
- id="security_reports.info"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/coding_rules",
- "query": Object {
- "types": "SECURITY_HOTSPOT,VULNERABILITY",
- },
+ <p
+ className="alert alert-info spacer-top display-inline-block"
+ >
+ <FormattedMessage
+ defaultMessage="security_reports.info"
+ id="security_reports.info"
+ tagName="p"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "types": "SECURITY_HOTSPOT,VULNERABILITY",
+ },
+ }
}
- }
- >
- security_reports.info.link
- </Link>,
+ >
+ security_reports.info.link
+ </Link>,
+ }
}
- }
- />
+ />
+ </p>
</div>
</header>
<div
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css b/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css
index e88cfb3e387..55d6afbfeda 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css
+++ b/server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css
@@ -37,20 +37,12 @@
margin: var(--gridSize) auto calc(2 * var(--gridSize));
}
-.sonarcloud-oauth-providers.oauth-providers > ul > li {
- margin-bottom: var(--gridSize);
-}
-
-.sonarcloud-oauth-providers.oauth-providers > ul > li > a > span {
- padding-left: calc(1.5 * var(--gridSize));
+.sonarcloud-oauth-providers.oauth-providers > ul {
+ width: 174px;
}
-.sonarcloud-oauth-providers.oauth-providers > ul > li > a > span::before {
- content: '';
- border-left: 1px var(--gray71) solid;
- height: 10px;
- opacity: 0.4;
- margin-right: calc(1.5 * var(--gridSize));
+.sonarcloud-oauth-providers.oauth-providers > ul > li {
+ margin-bottom: var(--gridSize);
}
.sonarcloud-oauth-providers.oauth-providers .oauth-providers-help {
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css b/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css
index 696beeb9cdc..050aca6bc4f 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css
+++ b/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.oauth-providers > ul {
- width: 180px;
+ width: 200px;
margin-left: auto;
margin-right: auto;
}
@@ -28,39 +28,6 @@
margin-bottom: 30px;
}
-.oauth-providers > ul > li > a {
- display: block;
- width: 180px;
- line-height: 22px;
- padding: 8px 12px;
- border: 1px solid rgba(0, 0, 0, 0.15);
- border-radius: 2px;
- box-sizing: border-box;
- background-color: var(--darkBlue);
- color: #fff;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.oauth-providers > ul > li > a:hover,
-.oauth-providers > ul > li > a:focus {
- box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.1);
-}
-
-.oauth-providers > ul > li > a.dark-text {
- color: var(--secondFontColor);
-}
-
-.oauth-providers > ul > li > a.dark-text:hover,
-.oauth-providers > ul > li > a.dark-text:focus {
- box-shadow: inset 0 0 0 100px rgba(0, 0, 0, 0.1);
-}
-
-.oauth-providers > ul > li > a > span {
- padding-left: 6px;
-}
-
.oauth-providers-help {
position: absolute;
top: 15px;
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx b/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx
index 1ecf27de26e..80ad700666e 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx
@@ -19,11 +19,11 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
-import { translateWithParameters } from '../../../helpers/l10n';
-import { IdentityProvider } from '../../../app/types';
import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { isDarkColor } from '../../../helpers/colors';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import { translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
+import { IdentityProvider } from '../../../app/types';
import './OAuthProviders.css';
interface Props {
@@ -58,25 +58,16 @@ interface ItemProps {
}
function OAuthProvider({ format, identityProvider, returnTo }: ItemProps) {
- const hasDarkBackground = isDarkColor(identityProvider.backgroundColor);
-
return (
<li>
- <a
- className={classNames({ 'dark-text': !hasDarkBackground })}
- href={
+ <IdentityProviderLink
+ identityProvider={identityProvider}
+ url={
`${getBaseUrl()}/sessions/init/${identityProvider.key}` +
`?return_to=${encodeURIComponent(returnTo)}`
- }
- style={{ backgroundColor: identityProvider.backgroundColor }}>
- <img
- alt={identityProvider.name}
- height="20"
- src={getBaseUrl() + identityProvider.iconPath}
- width="20"
- />
+ }>
<span>{format(identityProvider.name)}</span>
- </a>
+ </IdentityProviderLink>
{identityProvider.helpMessage && (
<HelpTooltip className="oauth-providers-help" overlay={identityProvider.helpMessage} />
)}
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap
index 7ab0639e474..e8da4c505fe 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap
@@ -38,49 +38,42 @@ exports[`should render correctly 1`] = `
exports[`should render correctly 2`] = `
<li>
- <a
- className=""
- href="/sessions/init/foo?return_to="
- style={
+ <IdentityProviderLink
+ identityProvider={
Object {
"backgroundColor": "#000",
+ "iconPath": "/some/path",
+ "key": "foo",
+ "name": "Foo",
}
}
+ url="/sessions/init/foo?return_to="
>
- <img
- alt="Foo"
- height="20"
- src="/some/path"
- width="20"
- />
<span>
login.login_with_x.Foo
</span>
- </a>
+ </IdentityProviderLink>
</li>
`;
exports[`should render correctly 3`] = `
<li>
- <a
- className=""
- href="/sessions/init/bar?return_to="
- style={
+ <IdentityProviderLink
+ identityProvider={
Object {
"backgroundColor": "#00F",
+ "helpMessage": "Help message!",
+ "iconPath": "/icon/path",
+ "key": "bar",
+ "name": "Bar",
}
}
+ url="/sessions/init/bar?return_to="
>
- <img
- alt="Bar"
- height="20"
- src="/icon/path"
- width="20"
- />
<span>
login.login_with_x.Bar
</span>
- </a>
+ </IdentityProviderLink>
<HelpTooltip
className="oauth-providers-help"
overlay="Help message!"
@@ -90,24 +83,20 @@ exports[`should render correctly 3`] = `
exports[`should use the custom label formatter 1`] = `
<li>
- <a
- className=""
- href="/sessions/init/foo?return_to="
- style={
+ <IdentityProviderLink
+ identityProvider={
Object {
"backgroundColor": "#000",
+ "iconPath": "/some/path",
+ "key": "foo",
+ "name": "Foo",
}
}
+ url="/sessions/init/foo?return_to="
>
- <img
- alt="Foo"
- height="20"
- src="/some/path"
- width="20"
- />
<span>
custom_format.Foo
</span>
- </a>
+ </IdentityProviderLink>
</li>
`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx b/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
index e6a6d887bd3..e7edd09e99f 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
@@ -18,19 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
import Modal from '../../components/controls/Modal';
-import { ResetButtonLink, Button } from '../../components/ui/buttons';
+import OnboardingPrivateIcon from '../../components/icons-components/OnboardingPrivateIcon';
+import OnboardingProjectIcon from '../../components/icons-components/OnboardingProjectIcon';
+import OnboardingTeamIcon from '../../components/icons-components/OnboardingTeamIcon';
+import { Button, ResetButtonLink } from '../../components/ui/buttons';
import { translate } from '../../helpers/l10n';
import { CurrentUser, isLoggedIn } from '../../app/types';
import { getCurrentUser } from '../../store/rootReducer';
import './styles.css';
interface OwnProps {
- onFinish: () => void;
+ onClose: (doSkipOnboarding?: boolean) => void;
onOpenOrganizationOnboarding: () => void;
- onOpenProjectOnboarding: () => void;
onOpenTeamOnboarding: () => void;
}
@@ -41,12 +44,25 @@ interface StateProps {
type Props = OwnProps & StateProps;
export class Onboarding extends React.PureComponent<Props> {
+ static contextTypes = {
+ router: PropTypes.object
+ };
+
componentDidMount() {
if (!isLoggedIn(this.props.currentUser)) {
handleRequiredAuthentication();
}
}
+ openProjectOnboarding = () => {
+ this.props.onClose(false);
+ this.context.router.push('/onboarding');
+ };
+
+ onFinish = () => {
+ this.props.onClose(true);
+ };
+
render() {
if (!isLoggedIn(this.props.currentUser)) {
return null;
@@ -57,41 +73,35 @@ export class Onboarding extends React.PureComponent<Props> {
<Modal
contentLabel={header}
medium={true}
- onRequestClose={this.props.onFinish}
+ onRequestClose={this.onFinish}
shouldCloseOnOverlayClick={false}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <div className="modal-body">
- <p className="spacer-top big-spacer-bottom">
- {translate('onboarding.header.description')}
- </p>
- <ul className="onboarding-choices">
- <li className="text-center">
- <p className="big-spacer-bottom">{translate('onboarding.analyze_public_code')}</p>
- <Button onClick={this.props.onOpenProjectOnboarding}>
- {translate('onboarding.analyze_public_code.button')}
- </Button>
- </li>
- <li className="text-center">
- <p className="big-spacer-bottom">{translate('onboarding.analyze_private_code')}</p>
- <Button onClick={this.props.onOpenOrganizationOnboarding}>
- {translate('onboarding.analyze_private_code.button')}
- </Button>
- </li>
- <li className="text-center">
- <p className="big-spacer-bottom">
- {translate('onboarding.contribute_existing_project')}
- </p>
- <Button onClick={this.props.onOpenTeamOnboarding}>
- {translate('onboarding.contribute_existing_project.button')}
- </Button>
- </li>
- </ul>
+ <div className="modal-simple-head text-center">
+ <h1>{translate('onboarding.header')}</h1>
+ <p className="spacer-top">{translate('onboarding.header.description')}</p>
+ </div>
+ <div className="modal-simple-body text-center onboarding-choices">
+ <Button className="onboarding-choice" onClick={this.openProjectOnboarding}>
+ <OnboardingProjectIcon />
+ <span>{translate('onboarding.analyze_public_code')}</span>
+ <p className="note">{translate('onboarding.analyze_public_code.note')}</p>
+ </Button>
+ <Button className="onboarding-choice" onClick={this.props.onOpenOrganizationOnboarding}>
+ <OnboardingPrivateIcon />
+ <span>{translate('onboarding.analyze_private_code')}</span>
+ <p className="note">{translate('onboarding.analyze_private_code.note')}</p>
+ </Button>
+ <Button className="onboarding-choice" onClick={this.props.onOpenTeamOnboarding}>
+ <OnboardingTeamIcon />
+ <span>{translate('onboarding.contribute_existing_project')}</span>
+ <p className="note">{translate('onboarding.contribute_existing_project.note')}</p>
+ </Button>
+ </div>
+ <div className="modal-simple-footer text-center">
+ <ResetButtonLink className="spacer-bottom" onClick={this.onFinish}>
+ {translate('not_now')}
+ </ResetButtonLink>
+ <p className="note">{translate('onboarding.footer')}</p>
</div>
- <footer className="modal-foot">
- <ResetButtonLink onClick={this.props.onFinish}>{translate('close')}</ResetButtonLink>
- </footer>
</Modal>
);
}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
index c9b3e77500e..d0350d43cc0 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
@@ -27,9 +27,8 @@ it('renders correctly', () => {
shallow(
<Onboarding
currentUser={{ isLoggedIn: true }}
- onFinish={jest.fn()}
+ onClose={jest.fn()}
onOpenOrganizationOnboarding={jest.fn()}
- onOpenProjectOnboarding={jest.fn()}
onOpenTeamOnboarding={jest.fn()}
/>
)
@@ -37,25 +36,25 @@ it('renders correctly', () => {
});
it('should correctly open the different tutorials', () => {
- const onFinish = jest.fn();
+ const onClose = jest.fn();
const onOpenOrganizationOnboarding = jest.fn();
- const onOpenProjectOnboarding = jest.fn();
const onOpenTeamOnboarding = jest.fn();
+ const push = jest.fn();
const wrapper = shallow(
<Onboarding
currentUser={{ isLoggedIn: true }}
- onFinish={onFinish}
+ onClose={onClose}
onOpenOrganizationOnboarding={onOpenOrganizationOnboarding}
- onOpenProjectOnboarding={onOpenProjectOnboarding}
onOpenTeamOnboarding={onOpenTeamOnboarding}
- />
+ />,
+ { context: { router: { push } } }
);
click(wrapper.find('ResetButtonLink'));
- expect(onFinish).toHaveBeenCalled();
+ expect(onClose).toHaveBeenCalled();
wrapper.find('Button').forEach(button => click(button));
expect(onOpenOrganizationOnboarding).toHaveBeenCalled();
- expect(onOpenProjectOnboarding).toHaveBeenCalled();
expect(onOpenTeamOnboarding).toHaveBeenCalled();
+ expect(push).toHaveBeenCalledWith('/onboarding');
});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
index e017f778601..e0a56e3fc79 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
@@ -4,79 +4,81 @@ exports[`renders correctly 1`] = `
<Modal
contentLabel="onboarding.header"
medium={true}
- onRequestClose={[MockFunction]}
+ onRequestClose={[Function]}
shouldCloseOnOverlayClick={false}
>
- <header
- className="modal-head"
- >
- <h2>
- onboarding.header
- </h2>
- </header>
<div
- className="modal-body"
+ className="modal-simple-head text-center"
>
+ <h1>
+ onboarding.header
+ </h1>
<p
- className="spacer-top big-spacer-bottom"
+ className="spacer-top"
>
onboarding.header.description
</p>
- <ul
- className="onboarding-choices"
+ </div>
+ <div
+ className="modal-simple-body text-center onboarding-choices"
+ >
+ <Button
+ className="onboarding-choice"
+ onClick={[Function]}
>
- <li
- className="text-center"
+ <OnboardingProjectIcon />
+ <span>
+ onboarding.analyze_public_code
+ </span>
+ <p
+ className="note"
>
- <p
- className="big-spacer-bottom"
- >
- onboarding.analyze_public_code
- </p>
- <Button
- onClick={[MockFunction]}
- >
- onboarding.analyze_public_code.button
- </Button>
- </li>
- <li
- className="text-center"
+ onboarding.analyze_public_code.note
+ </p>
+ </Button>
+ <Button
+ className="onboarding-choice"
+ onClick={[MockFunction]}
+ >
+ <OnboardingPrivateIcon />
+ <span>
+ onboarding.analyze_private_code
+ </span>
+ <p
+ className="note"
>
- <p
- className="big-spacer-bottom"
- >
- onboarding.analyze_private_code
- </p>
- <Button
- onClick={[MockFunction]}
- >
- onboarding.analyze_private_code.button
- </Button>
- </li>
- <li
- className="text-center"
+ onboarding.analyze_private_code.note
+ </p>
+ </Button>
+ <Button
+ className="onboarding-choice"
+ onClick={[MockFunction]}
+ >
+ <OnboardingTeamIcon />
+ <span>
+ onboarding.contribute_existing_project
+ </span>
+ <p
+ className="note"
>
- <p
- className="big-spacer-bottom"
- >
- onboarding.contribute_existing_project
- </p>
- <Button
- onClick={[MockFunction]}
- >
- onboarding.contribute_existing_project.button
- </Button>
- </li>
- </ul>
+ onboarding.contribute_existing_project.note
+ </p>
+ </Button>
</div>
- <footer
- className="modal-foot"
+ <div
+ className="modal-simple-footer text-center"
>
<ResetButtonLink
- onClick={[MockFunction]}
+ className="spacer-bottom"
+ onClick={[Function]}
>
- close
+ not_now
</ResetButtonLink>
- </footer>
+ <p
+ className="note"
+ >
+ onboarding.footer
+ </p>
+ </div>
</Modal>
`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx
new file mode 100644
index 00000000000..58c86e34696
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import { getIdentityProviders } from '../../../api/users';
+import { getRepositories } from '../../../api/alm-integration';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { IdentityProvider, LoggedInUser } from '../../../app/types';
+
+interface Props {
+ currentUser: LoggedInUser;
+}
+
+interface State {
+ identityProviders: IdentityProvider[];
+ installationUrl?: string;
+ installed?: boolean;
+ loading: boolean;
+}
+
+export default class AutoProjectCreate extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { identityProviders: [], loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ Promise.all([this.fetchIdentityProviders(), this.fetchRepositories()]).then(
+ this.stopLoading,
+ this.stopLoading
+ );
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchIdentityProviders = () => {
+ return getIdentityProviders().then(
+ ({ identityProviders }) => {
+ if (this.mounted) {
+ this.setState({ identityProviders });
+ }
+ },
+ () => {
+ return Promise.resolve();
+ }
+ );
+ };
+
+ fetchRepositories = () => {
+ return getRepositories().then(({ installation }) => {
+ if (this.mounted) {
+ this.setState({
+ installationUrl: installation.installationUrl,
+ installed: installation.enabled
+ });
+ }
+ });
+ };
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ render() {
+ if (this.state.loading) {
+ return <DeferredSpinner />;
+ }
+
+ const { currentUser } = this.props;
+ const identityProvider = this.state.identityProviders.find(
+ identityProvider => identityProvider.key === currentUser.externalProvider
+ );
+
+ if (!identityProvider) {
+ return null;
+ }
+
+ return (
+ <>
+ <p className="alert alert-info width-60 big-spacer-bottom">
+ {translateWithParameters(
+ 'onboarding.create_project.beta_feature_x',
+ identityProvider.name
+ )}
+ </p>
+ {this.state.installed ? (
+ 'Repositories list'
+ ) : (
+ <div>
+ <p className="spacer-bottom">
+ {translateWithParameters(
+ 'onboarding.create_project.install_app_x',
+ identityProvider.name
+ )}
+ </p>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={identityProvider}
+ small={true}
+ url={this.state.installationUrl}>
+ {translateWithParameters(
+ 'onboarding.create_project.install_app_x.button',
+ identityProvider.name
+ )}
+ </IdentityProviderLink>
+ </div>
+ )}
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx
new file mode 100644
index 00000000000..7803335143c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx
@@ -0,0 +1,182 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import * as PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import Helmet from 'react-helmet';
+import AutoProjectCreate from './AutoProjectCreate';
+import ManualProjectCreate from './ManualProjectCreate';
+import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import { getCurrentUser } from '../../../store/rootReducer';
+import { skipOnboarding } from '../../../store/users/actions';
+import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { ProjectBase } from '../../../api/components';
+import { getProjectUrl, getOrganizationUrl } from '../../../helpers/urls';
+import '../../../app/styles/sonarcloud.css';
+import '../styles.css';
+
+interface OwnProps {
+ onFinishOnboarding: () => void;
+}
+
+interface StateProps {
+ currentUser: CurrentUser;
+}
+
+interface DispatchProps {
+ skipOnboarding: () => void;
+}
+
+enum Tabs {
+ AUTO,
+ MANUAL
+}
+
+type Props = OwnProps & StateProps & DispatchProps;
+
+interface State {
+ activeTab: Tabs;
+}
+
+export class CreateProjectOnboarding extends React.PureComponent<Props, State> {
+ mounted = false;
+ static contextTypes = {
+ router: PropTypes.object
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { activeTab: this.shouldDisplayTabs(props) ? Tabs.AUTO : Tabs.MANUAL };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ if (!isLoggedIn(this.props.currentUser)) {
+ handleRequiredAuthentication();
+ }
+ document.body.classList.add('white-page');
+ document.documentElement.classList.add('white-page');
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ document.body.classList.remove('white-page');
+ document.documentElement.classList.remove('white-page');
+ }
+
+ handleProjectCreate = (projects: Pick<ProjectBase, 'key'>[], organization?: string) => {
+ if (projects.length > 1 && organization) {
+ this.context.router.push(getOrganizationUrl(organization) + '/projects');
+ } else if (projects.length === 1) {
+ this.context.router.push(getProjectUrl(projects[0].key));
+ }
+ };
+
+ shouldDisplayTabs = ({ currentUser } = this.props) => {
+ return (
+ isLoggedIn(currentUser) &&
+ ['bitbucket', 'github'].includes(currentUser.externalProvider || '')
+ );
+ };
+
+ showAuto = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.setState({ activeTab: Tabs.AUTO });
+ };
+
+ showManual = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.setState({ activeTab: Tabs.MANUAL });
+ };
+
+ render() {
+ const { currentUser } = this.props;
+ if (!isLoggedIn(currentUser)) {
+ return null;
+ }
+
+ const { activeTab } = this.state;
+ const header = translate('onboarding.create_project.header');
+ return (
+ <>
+ <Helmet title={header} titleTemplate="%s" />
+ <div className="sonarcloud page page-limited">
+ <div className="page-header">
+ <h1 className="page-title">{header}</h1>
+ </div>
+
+ {this.shouldDisplayTabs() && (
+ <ul className="flex-tabs">
+ <li>
+ <a
+ className={classNames('js-auto', { selected: activeTab === Tabs.AUTO })}
+ href="#"
+ onClick={this.showAuto}>
+ {translate('onboarding.create_project.select_repositories')}
+ <span
+ className={classNames(
+ 'rounded alert alert-small spacer-left display-inline-block',
+ {
+ 'alert-info': activeTab === Tabs.AUTO,
+ 'alert-muted': activeTab !== Tabs.AUTO
+ }
+ )}>
+ {translate('beta')}
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ className={classNames('js-manual', { selected: activeTab === Tabs.MANUAL })}
+ href="#"
+ onClick={this.showManual}>
+ {translate('onboarding.create_project.create_manually')}
+ </a>
+ </li>
+ </ul>
+ )}
+
+ {activeTab === Tabs.AUTO ? (
+ <AutoProjectCreate currentUser={currentUser} />
+ ) : (
+ <ManualProjectCreate
+ currentUser={currentUser}
+ onProjectCreate={this.handleProjectCreate}
+ />
+ )}
+ </div>
+ </>
+ );
+ }
+}
+
+const mapStateToProps = (state: any): StateProps => {
+ return {
+ currentUser: getCurrentUser(state)
+ };
+};
+
+const mapDispatchToProps: DispatchProps = { skipOnboarding };
+
+export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
+ CreateProjectOnboarding
+);
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx
new file mode 100644
index 00000000000..59d615583c7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx
@@ -0,0 +1,227 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { sortBy } from 'lodash';
+import { connect } from 'react-redux';
+import CreateOrganizationForm from '../../account/organizations/CreateOrganizationForm';
+import Select from '../../../components/controls/Select';
+import { Button, SubmitButton } from '../../../components/ui/buttons';
+import { LoggedInUser, Organization } from '../../../app/types';
+import { fetchMyOrganizations } from '../../account/organizations/actions';
+import { getMyOrganizations } from '../../../store/rootReducer';
+import { translate } from '../../../helpers/l10n';
+import { createProject, ProjectBase } from '../../../api/components';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+
+interface StateProps {
+ userOrganizations: Organization[];
+}
+
+interface DispatchProps {
+ fetchMyOrganizations: () => Promise<void>;
+}
+
+interface OwnProps {
+ currentUser: LoggedInUser;
+ onProjectCreate: (project: ProjectBase[]) => void;
+}
+
+type Props = OwnProps & StateProps & DispatchProps;
+
+interface State {
+ createOrganizationModal: boolean;
+ projectName: string;
+ projectKey: string;
+ selectedOrganization: string;
+ submitting: boolean;
+}
+
+export class ManualProjectCreate extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ createOrganizationModal: false,
+ projectName: '',
+ projectKey: '',
+ selectedOrganization:
+ props.userOrganizations.length <= 1 ? props.userOrganizations[0].key : '',
+ submitting: false
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ closeCreateOrganization = () => {
+ this.setState({ createOrganizationModal: false });
+ };
+
+ handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
+ if (this.isValid()) {
+ const { projectKey, projectName, selectedOrganization } = this.state;
+ this.setState({ submitting: true });
+ createProject({
+ project: projectKey,
+ name: projectName,
+ organization: selectedOrganization
+ }).then(
+ ({ project }) => this.props.onProjectCreate([project]),
+ () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ }
+ );
+ }
+ };
+
+ handleOrganizationSelect = ({ value }: { value: string }) => {
+ this.setState({ selectedOrganization: value });
+ };
+
+ handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ projectName: event.currentTarget.value });
+ };
+
+ handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ projectKey: event.currentTarget.value });
+ };
+
+ isValid = () => {
+ const { projectKey, projectName, selectedOrganization } = this.state;
+ return Boolean(projectKey && projectName && selectedOrganization);
+ };
+
+ onCreateOrganization = (organization: { key: string }) => {
+ this.props.fetchMyOrganizations().then(
+ () => {
+ this.handleOrganizationSelect({ value: organization.key });
+ this.closeCreateOrganization();
+ },
+ () => {
+ this.closeCreateOrganization();
+ }
+ );
+ };
+
+ showCreateOrganization = () => {
+ this.setState({ createOrganizationModal: true });
+ };
+
+ render() {
+ const { submitting } = this.state;
+ return (
+ <>
+ <form onSubmit={this.handleFormSubmit}>
+ <div className="form-field">
+ <label htmlFor="select-organization">
+ {translate('onboarding.create_project.organization')}
+ <em className="mandatory">*</em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ onChange={this.handleOrganizationSelect}
+ options={sortBy(this.props.userOrganizations, o => o.name.toLowerCase()).map(
+ organization => ({
+ label: organization.name,
+ value: organization.key
+ })
+ )}
+ required={true}
+ value={this.state.selectedOrganization}
+ />
+ <Button
+ className="button-link big-spacer-left js-new-org"
+ onClick={this.showCreateOrganization}>
+ {translate('onboarding.create_project.create_new_org')}
+ </Button>
+ </div>
+ <div className="form-field">
+ <label htmlFor="project-name">
+ {translate('onboarding.create_project.project_name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={400}
+ minLength={1}
+ onChange={this.handleProjectNameChange}
+ required={true}
+ type="text"
+ value={this.state.projectName}
+ />
+ </div>
+ <div className="form-field">
+ <label htmlFor="project-key">
+ {translate('onboarding.create_project.project_key')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={this.handleProjectKeyChange}
+ required={true}
+ type="text"
+ value={this.state.projectKey}
+ />
+ </div>
+ <SubmitButton disabled={!this.isValid() || submitting}>
+ {translate('onboarding.create_project.create_project')}
+ </SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={submitting} />
+ </form>
+ {this.state.createOrganizationModal && (
+ <CreateOrganizationForm
+ onClose={this.closeCreateOrganization}
+ onCreate={this.onCreateOrganization}
+ />
+ )}
+ </>
+ );
+ }
+}
+
+const mapDispatchToProps = ({
+ fetchMyOrganizations
+} as any) as DispatchProps;
+
+const mapStateToProps = (state: any): StateProps => {
+ return {
+ userOrganizations: getMyOrganizations(state)
+ };
+};
+export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
+ ManualProjectCreate
+);
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx
new file mode 100644
index 00000000000..10ea4c7e215
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import AutoProjectCreate from '../AutoProjectCreate';
+import { getIdentityProviders } from '../../../../api/users';
+import { getRepositories } from '../../../../api/alm-integration';
+import { LoggedInUser } from '../../../../app/types';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/users', () => ({
+ getIdentityProviders: jest.fn().mockResolvedValue({
+ identityProviders: [
+ {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ key: 'foo',
+ name: 'Foo Provider'
+ }
+ ]
+ })
+}));
+
+jest.mock('../../../../api/alm-integration', () => ({
+ getRepositories: jest.fn().mockResolvedValue({
+ installation: {
+ installationUrl: 'https://alm.foo.com/install',
+ enabled: false
+ }
+ })
+}));
+
+const user: LoggedInUser = { isLoggedIn: true, login: 'foo', name: 'Foo', externalProvider: 'foo' };
+
+beforeEach(() => {
+ (getIdentityProviders as jest.Mock<any>).mockClear();
+ (getRepositories as jest.Mock<any>).mockClear();
+});
+
+it('should display the provider app install button', async () => {
+ const wrapper = getWrapper();
+ expect(wrapper).toMatchSnapshot();
+ expect(getIdentityProviders).toHaveBeenCalled();
+ expect(getRepositories).toHaveBeenCalled();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(<AutoProjectCreate currentUser={user} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx
new file mode 100644
index 00000000000..f7cfa3ce8da
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { CreateProjectOnboarding } from '../CreateProjectOnboarding';
+import { LoggedInUser } from '../../../../app/types';
+import { click } from '../../../../helpers/testUtils';
+
+const user: LoggedInUser = {
+ externalProvider: 'github',
+ isLoggedIn: true,
+ login: 'foo',
+ name: 'Foo'
+};
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should render with Manual creation only', () => {
+ expect(getWrapper({ currentUser: { ...user, externalProvider: 'vsts' } })).toMatchSnapshot();
+});
+
+it('should switch tabs', () => {
+ const wrapper = getWrapper();
+ click(wrapper.find('.js-manual'));
+ expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy();
+ click(wrapper.find('.js-auto'));
+ expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <CreateProjectOnboarding
+ currentUser={user}
+ onFinishOnboarding={jest.fn()}
+ skipOnboarding={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx
new file mode 100644
index 00000000000..b79b4e4ae35
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { ManualProjectCreate } from '../ManualProjectCreate';
+import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils';
+import { createProject } from '../../../../api/components';
+
+jest.mock('../../../../api/components', () => ({
+ createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
+}));
+
+beforeEach(() => {
+ (createProject as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should allow to create a new org', async () => {
+ const fetchMyOrganizations = jest.fn().mockResolvedValueOnce([]);
+ const wrapper = getWrapper({ fetchMyOrganizations });
+
+ click(wrapper.find('.js-new-org'));
+ const createForm = wrapper.find('Connect(CreateOrganizationForm)');
+ expect(createForm.exists()).toBeTruthy();
+
+ createForm.prop<Function>('onCreate')({ key: 'baz' });
+ expect(fetchMyOrganizations).toHaveBeenCalled();
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state('selectedOrganization')).toBe('baz');
+});
+
+it('should correctly create a project', async () => {
+ const onProjectCreate = jest.fn();
+ const wrapper = getWrapper({ onProjectCreate });
+ wrapper.find('Select').prop<Function>('onChange')({ value: 'foo' });
+ change(wrapper.find('#project-name'), 'Bar');
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+ change(wrapper.find('#project-key'), 'bar');
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+ submit(wrapper.find('form'));
+ expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
+
+ await waitAndUpdate(wrapper);
+ expect(onProjectCreate).toBeCalledWith([{ key: 'bar', name: 'Bar' }]);
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <ManualProjectCreate
+ currentUser={{ isLoggedIn: true, login: 'foo', name: 'Foo' }}
+ fetchMyOrganizations={jest.fn()}
+ onProjectCreate={jest.fn()}
+ userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
new file mode 100644
index 00000000000..9320a251046
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the provider app install button 1`] = `
+<DeferredSpinner
+ timeout={100}
+/>
+`;
+
+exports[`should display the provider app install button 2`] = `
+<React.Fragment>
+ <p
+ className="alert alert-info width-60 big-spacer-bottom"
+ >
+ onboarding.create_project.beta_feature_x.Foo Provider
+ </p>
+ <div>
+ <p
+ className="spacer-bottom"
+ >
+ onboarding.create_project.install_app_x.Foo Provider
+ </p>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "key": "foo",
+ "name": "Foo Provider",
+ }
+ }
+ small={true}
+ url="https://alm.foo.com/install"
+ >
+ onboarding.create_project.install_app_x.button.Foo Provider
+ </IdentityProviderLink>
+ </div>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap
new file mode 100644
index 00000000000..9542df43fb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap
@@ -0,0 +1,97 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<React.Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </div>
+ <ul
+ className="flex-tabs"
+ >
+ <li>
+ <a
+ className="js-auto selected"
+ href="#"
+ onClick={[Function]}
+ >
+ onboarding.create_project.select_repositories
+ <span
+ className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ >
+ beta
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ className="js-manual"
+ href="#"
+ onClick={[Function]}
+ >
+ onboarding.create_project.create_manually
+ </a>
+ </li>
+ </ul>
+ <AutoProjectCreate
+ currentUser={
+ Object {
+ "externalProvider": "github",
+ "isLoggedIn": true,
+ "login": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ </div>
+</React.Fragment>
+`;
+
+exports[`should render with Manual creation only 1`] = `
+<React.Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </div>
+ <Connect(ManualProjectCreate)
+ currentUser={
+ Object {
+ "externalProvider": "vsts",
+ "isLoggedIn": true,
+ "login": "foo",
+ "name": "Foo",
+ }
+ }
+ onProjectCreate={[Function]}
+ />
+ </div>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
new file mode 100644
index 00000000000..fafb751c9bb
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly create a project 1`] = `
+<SubmitButton
+ disabled={true}
+>
+ onboarding.create_project.create_project
+</SubmitButton>
+`;
+
+exports[`should correctly create a project 2`] = `
+<SubmitButton
+ disabled={false}
+>
+ onboarding.create_project.create_project
+</SubmitButton>
+`;
+
+exports[`should render correctly 1`] = `
+<React.Fragment>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="select-organization"
+ >
+ onboarding.create_project.organization
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Bar",
+ "value": "bar",
+ },
+ Object {
+ "label": "Foo",
+ "value": "foo",
+ },
+ ]
+ }
+ required={true}
+ value=""
+ />
+ <Button
+ className="button-link big-spacer-left js-new-org"
+ onClick={[Function]}
+ >
+ onboarding.create_project.create_new_org
+ </Button>
+ </div>
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="project-name"
+ >
+ onboarding.create_project.project_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={400}
+ minLength={1}
+ onChange={[Function]}
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="project-key"
+ >
+ onboarding.create_project.project_key
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[Function]}
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <SubmitButton
+ disabled={true}
+ >
+ onboarding.create_project.create_project
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/routes.ts b/server/sonar-web/src/main/js/apps/tutorials/routes.ts
new file mode 100644
index 00000000000..9f5b34ba428
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/routes.ts
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { lazyLoad } from '../../components/lazyLoad';
+import { isSonarCloud } from '../../helpers/system';
+
+const routes = [
+ {
+ indexRoute: {
+ component: lazyLoad(
+ () =>
+ isSonarCloud()
+ ? import('../../apps/tutorials/createProjectOnboarding/CreateProjectOnboarding')
+ : import('../../apps/tutorials/projectOnboarding/ProjectOnboardingPage')
+ )
+ }
+ }
+];
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/styles.css b/server/sonar-web/src/main/js/apps/tutorials/styles.css
index f73428e0f40..798fce21bcc 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/styles.css
+++ b/server/sonar-web/src/main/js/apps/tutorials/styles.css
@@ -58,5 +58,43 @@
.onboarding-choices {
display: flex;
justify-content: space-around;
- padding: 24px 0 44px;
+ padding-top: 44px;
+ padding-bottom: 44px;
+ background-color: var(--barBackgroundColor);
+}
+
+.onboarding-choice {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ padding: calc(2 * var(--gridSize));
+ width: 190px;
+ height: 190px;
+ background-color: #fff;
+ border: solid 1px #fff;
+ border-radius: 3px;
+ transition: all 0.2s ease;
+ box-shadow: 0 1px 1px 1px var(--barBorderColor);
+}
+
+.onboarding-choice svg {
+ color: var(--gray40);
+ margin-bottom: calc(3 * var(--gridSize));
+}
+
+.onboarding-choice span {
+ font-size: var(--mediumFontSize);
+ margin-bottom: calc(var(--gridSize) / 2);
+}
+
+.onboarding-choice .note {
+ font-weight: 400;
+}
+
+.onboarding-choice:hover,
+.onboarding-choice:focus,
+.onboarding-choice:active {
+ background-color: #fff;
+ box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.35);
+ color: var(--darkBlue);
}
diff --git a/server/sonar-web/src/main/js/components/controls/react-select.css b/server/sonar-web/src/main/js/components/controls/react-select.css
index b65548be61e..5cfcff66eae 100644
--- a/server/sonar-web/src/main/js/components/controls/react-select.css
+++ b/server/sonar-web/src/main/js/components/controls/react-select.css
@@ -172,6 +172,7 @@
margin: 0;
outline: none;
padding: 0;
+ box-shadow: none;
-webkit-appearance: none;
}
diff --git a/server/sonar-web/src/main/js/components/icons-components/OnboardingPrivateIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OnboardingPrivateIcon.tsx
new file mode 100644
index 00000000000..3592d9b0be0
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/OnboardingPrivateIcon.tsx
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function OnboardingPrivateIcon({
+ className,
+ fill = 'currentColor',
+ size
+}: IconProps) {
+ return (
+ <Icon className={className} size={size || 64} viewBox="">
+ <g fill="none" stroke={fill} strokeWidth="2">
+ <path d="M2 59h60V13H2zm0-46h60V5H2zm3-4h2m2 0h2m2 0h2m2 0h42" />
+ <path d="M59 34h-6l-2-4h-6l-2 5h-6l-2 2h-6l-2-4h-6l-2 5h-6l-2 4H5m1 14v-9m4 9v-6m4 6V43m4 13V45m4 11V42m4 14V39m4 17V41m4 15V46m4 10V40m4 16V44m4 12V37m4 19V38m4 18V43m4 13V39m-3-18h-2m-2 0h-2m-2 0h-2M9 29h14M9 33h7m17-12h8m-14 4h8m-8-4h4m-21 4h12v-4H10z" />
+ <path d="M58 31V17H6v22" />
+ <path
+ d="M50 36c0-9.389-7.611-17-17-17s-17 7.611-17 17 7.611 17 17 17 17-7.611 17-17"
+ fill="#FFF"
+ stroke="none"
+ />
+ <path d="M50 36c0-9.389-7.611-17-17-17s-17 7.611-17 17 7.611 17 17 17 17-7.611 17-17z" />
+ <mask fill="#FFF" id="a">
+ <path d="M0 56h62V0H0z" />
+ </mask>
+ <path
+ d="M27 45h12V33H27zm10-12v-4c0-1.023-.391-2.047-1.172-2.828C35.048 25.391 34.023 25 33 25s-2.048.391-2.828 1.172C29.391 26.953 29 27.977 29 29v4"
+ mask="url(#a)"
+ />
+ </g>
+ </Icon>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx
new file mode 100644
index 00000000000..d2090eed867
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function OnboardingProjectIcon({
+ className,
+ fill = 'currentColor',
+ size
+}: IconProps) {
+ return (
+ <Icon className={className} size={size || 64} viewBox="">
+ <g fill="none" fillRule="evenodd" stroke={fill} strokeWidth="2">
+ <path d="M2 59h60V13H2zm0-46h60V5H2zm3-4h2m2 0h2m2 0h2m2 0h42" />
+ <path d="M59 34h-6l-2-4h-6l-2 5h-6l-2 2h-6l-2-4h-6l-2 5h-6l-2 4H5m1 14v-9m4 9v-6m4 6V43m4 13V45m4 11V42m4 14V39m4 17V41m4 15V46m4 10V40m4 16V44m4 12V37m4 19V38m4 18V43m4 13V39m-3-18h-2m-2 0h-2m-2 0h-2M9 29h14M9 33h7m17-12h8m-14 4h8m-8-4h4m-21 4h12v-4H10z" />
+ <path d="M58 31V17H6v22" />
+ </g>
+ </Icon>
+ );
+}
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..6ce74838a0d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function OnboardingTeamIcon({ className, fill = 'currentColor', size }: IconProps) {
+ return (
+ <Icon className={className} size={size || 64} viewBox="">
+ <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/IdentityProviderLink.css b/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.css
new file mode 100644
index 00000000000..8277e368f2b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.css
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+a.identity-provider-link {
+ display: block;
+ width: auto;
+ line-height: 22px;
+ padding: var(--gridSize) calc(1.5 * var(--gridSize));
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 2px;
+ box-sizing: border-box;
+ background-color: var(--darkBlue);
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+a.identity-provider-link.small {
+ line-height: 14px;
+ padding: calc(var(--gridSize) / 2) var(--gridSize);
+}
+
+a.identity-provider-link:hover,
+a.identity-provider-link:focus {
+ box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.1);
+}
+
+a.identity-provider-link.dark-text {
+ color: var(--secondFontColor);
+}
+
+a.identity-provider-link.dark-text:hover,
+a.identity-provider-link.dark-text:focus {
+ box-shadow: inset 0 0 0 100px rgba(0, 0, 0, 0.1);
+}
+
+a.identity-provider-link > img {
+ padding-right: calc(1.5 * var(--gridSize));
+}
+
+a.identity-provider-link.small > img {
+ padding-right: var(--gridSize);
+}
+
+a.identity-provider-link > span::before {
+ content: '';
+ opacity: 0.4;
+ border-left: 1px var(--gray71) solid;
+ margin-right: calc(1.5 * var(--gridSize));
+}
diff --git a/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx b/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx
new file mode 100644
index 00000000000..9e4f87c5ed3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { isDarkColor } from '../../helpers/colors';
+import { getBaseUrl } from '../../helpers/urls';
+import { IdentityProvider } from '../../app/types';
+import './IdentityProviderLink.css';
+
+interface Props {
+ children: React.ReactNode;
+ className?: string;
+ identityProvider: IdentityProvider;
+ small?: boolean;
+ url: string | undefined;
+}
+
+export default function IdentityProviderLink({
+ children,
+ className,
+ identityProvider,
+ small,
+ url
+}: Props) {
+ const size = small ? 14 : 20;
+
+ return (
+ <a
+ className={classNames(
+ 'identity-provider-link',
+ { 'dark-text': !isDarkColor(identityProvider.backgroundColor), small },
+ className
+ )}
+ href={url}
+ style={{ backgroundColor: identityProvider.backgroundColor }}>
+ <img
+ alt={identityProvider.name}
+ height={size}
+ src={getBaseUrl() + identityProvider.iconPath}
+ width={size}
+ />
+ {children}
+ </a>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/IdentityProviderLink-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/IdentityProviderLink-test.tsx
new file mode 100644
index 00000000000..1a072dd00c4
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/IdentityProviderLink-test.tsx
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import IdentityProviderLink from '../IdentityProviderLink';
+
+const identityProvider = {
+ backgroundColor: '#000',
+ iconPath: '/some/path',
+ key: 'foo',
+ name: 'Foo'
+};
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <IdentityProviderLink identityProvider={identityProvider} url="/url/foo/bar">
+ Link text
+ </IdentityProviderLink>
+ )
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap
new file mode 100644
index 00000000000..d3a38a52376
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<a
+ className="identity-provider-link"
+ href="/url/foo/bar"
+ style={
+ Object {
+ "backgroundColor": "#000",
+ }
+ }
+>
+ <img
+ alt="Foo"
+ height={20}
+ src="/some/path"
+ width={20}
+ />
+ Link text
+</a>
+`;