]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11036 Install integration with GitHub or BitBucket Cloud
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 20 Jul 2018 14:57:23 +0000 (16:57 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 10 Aug 2018 18:21:28 +0000 (20:21 +0200)
* 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

53 files changed:
server/sonar-web/scripts/start.js
server/sonar-web/src/main/js/api/alm-integration.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
server/sonar-web/src/main/js/app/styles/components/alerts.css
server/sonar-web/src/main/js/app/styles/components/modals.css
server/sonar-web/src/main/js/app/styles/init/forms.css
server/sonar-web/src/main/js/app/styles/sonarcloud.css [new file with mode: 0644]
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/about/routes.ts
server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/projectsManagement/utils.ts [deleted file]
server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css
server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css
server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/styles.css
server/sonar-web/src/main/js/components/controls/react-select.css
server/sonar-web/src/main/js/components/icons-components/OnboardingPrivateIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/IdentityProviderLink.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/IdentityProviderLink-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index ce3726d43e9fe43245a47ab37701dac24db89df1..b1f4d4da3ca4afcae1c15bf512ba4dfb17ba146c 100644 (file)
@@ -111,7 +111,9 @@ function runDevServer(compiler, host, port, protocol) {
     proxy: {
       '/api': { target: proxy, changeOrigin: true },
       '/static': { target: proxy, changeOrigin: true },
-      '/integration': { target: proxy, changeOrigin: true }
+      '/integration': { target: proxy, changeOrigin: true },
+      '/sessions/init': { target: proxy, changeOrigin: true },
+      '/oauth2': { target: proxy, changeOrigin: true }
     }
   });
 
diff --git a/server/sonar-web/src/main/js/api/alm-integration.ts b/server/sonar-web/src/main/js/api/alm-integration.ts
new file mode 100644 (file)
index 0000000..0232632
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 { getJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
+
+export function getRepositories(): Promise<{
+  installation: {
+    installationUrl: string;
+    enabled: boolean;
+  };
+}> {
+  return getJSON('/api/alm_integration/list_repositories').catch(throwGlobalError);
+}
index a069c38b17e5203594abd335c0768fe4d05c7af7..c917f8a44601cda15380bf4dce513aab9186a9b1 100644 (file)
@@ -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);
 }
 
index bbd6c574eecacf038dfb6e6cf2994e7033533b6c..c49a77d20520b28f24896c459649195703c46c18 100644 (file)
@@ -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}
           />
         )}
index 412846db4668592366cfe3f836e1fede9b7e9124..58030362c11b408791ea494ebd90d36f1a12feec 100644 (file)
@@ -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" />
index 1664c15d58c038cae9ad4526b45ff10e37d75db5..56a23c2dc5eaa0ee29614e87379338f02beb5ce0 100644 (file)
@@ -12,7 +12,7 @@ exports[`render 1`] = `
           href="#"
           onClick={[Function]}
         >
-          my_account.analyze_new_project
+          provisioning.create_new_project
         </a>
       </li>
       <li
index ce827c16d036ff02dab4fb0b854d6da287d5cf04..b965dced2d66e16de4cb298c36ea4b64ce2fac9d 100644 (file)
   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 {
index 1e958e4b5267328fe8262484f9665b75b52ab9ef..987557266615160b77223dcab19d5d8c32ffa178 100644 (file)
   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 {
index 881e78fe80405cbb71df52b64c81c96829a97ca3..93a7d5c7a151b90fa8a7764b57e099ad03244c3a 100644 (file)
@@ -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 (file)
index 0000000..66a4f2f
--- /dev/null
@@ -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);
+}
index 91ba51aa4c3353b2bcfc3c85a4c1535877414a20..0a90beef99bc19637cee0357f1d2ef280685f92e 100644 (file)
@@ -64,6 +64,7 @@ module.exports = {
   smallFontSize: '12px',
   mediumFontSize: '14px',
   bigFontSize: '16px',
+  hugeFontSize: '24px',
 
   controlHeight: `${3 * grid}px`,
   smallControlHeight: `${2.5 * grid}px`,
index e02ced80d7e6450598bbad636fdf6093959727e7..4f1b00c3f761bf9f01951e8a7cd818f5ee0233db 100644 (file)
@@ -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;
index 69602ff1e134bf7f2107989c6caa93a15d104b11..788cd86620a0d13e49a7a29a9795b1981c3ed0b1 100644 (file)
@@ -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} />
index 315b604da61f0f1f8c2f1b4fa8e0b284c847f8fd..c41f05acfc22ddbb6ae54892750290024befb0fa 100644 (file)
@@ -27,7 +27,7 @@ const routes = [
         () => (isSonarCloud() ? import('./sonarcloud/Home') : import('./components/AboutApp'))
       )
     },
-    childRoutes: isSonarCloud
+    childRoutes: isSonarCloud()
       ? [
           {
             path: 'contact',
index 89e595feed43cb0f6a569014b77aef4b9077d6d3..2148e50098fa612a6bfa67f02708e1ea1aa747cd 100644 (file)
@@ -34,7 +34,7 @@ export default class UserExternalIdentity extends React.PureComponent {
 
   componentDidUpdate(prevProps) {
     if (prevProps.user !== this.props.user) {
-      this.this.fetchIdentityProviders();
+      this.fetchIdentityProviders();
     }
   }
 
index 4cd7b7d2164da90224a8003a90e862f246e22ede..c572739793356a5171e983f35fa57dd73c277eb1 100644 (file)
@@ -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"
index 8cf6f2034964228385c29c39640e6c3c19cd6d65..a73ab9e50444d756961eb118815391f804545c60 100644 (file)
@@ -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"
index 2a5bdbeee2738c0ac424f2b22c882e2f1df524b9..4df65fc78dfc9cc69998f6291474d229fdf6ade1 100644 (file)
@@ -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 = () => {
index 8853af4a079e4532fca5ad43e540d103e73d7324..7f1b12c3e4ddec89e9b4f5d804b5280c1a5a59a0 100644 (file)
 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 };
index 438db5c8a777bed2d4a7ca8994b6956ac7f88ffb..5932a17e570f6bd3b81f958705a85737f12f9918 100644 (file)
@@ -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';
index 7e72fb9b057d236f2f96ec742995dc7bb474d8a3..09add750d945aab95ca31f5185aad8a57a3e1ec5 100644 (file)
@@ -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 };
index 57183554451cb7e6ea408eb3f4a04339c466620f..7c402913a250a1dc16267bf8e90f5ae7460a017b 100644 (file)
  */
 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 };
index 205526b58aa5e63276b7b8a83fa3d72a97ca6fe1..f336153d9760966a2dcd4aefe3ca437ace14c553 100644 (file)
@@ -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/projectsManagement/utils.ts b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts
deleted file mode 100644 (file)
index 183b47b..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 { SearchProjectsResponseComponent } from '../../api/components';
-
-export const PAGE_SIZE = 50;
-
-export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];
-
-export type Project = SearchProjectsResponseComponent;
index fa966581a944119e9541cd49394c209f4c073447..def1019b4b4fa192669b9d4c1db8c8e8707f31f7 100755 (executable)
@@ -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">
index b787a7c99a82d5780ab79734497bc36cbbca09c8..86b8d8c2b576b834e501ea7b0699ef70b12d8ece 100644 (file)
@@ -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
index e88cfb3e3870015772c95315cad8a8798fbd1212..55d6afbfedac27231619ee681a549ad54cc68ce8 100644 (file)
   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 {
index 696beeb9cdc5098459729d12248d406d70ace3e5..050aca6bc4f5a32ae76cbd89ddad481fdf10d8eb 100644 (file)
@@ -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;
 }
   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;
index 1ecf27de26e9b0b1a1228986ce64683e5867130c..80ad700666ed66740ed3cd39d3460b102056ee0e 100644 (file)
  */
 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} />
       )}
index 7ab0639e4740c3b7d8f15ef0ece1add60bf4ee85..e8da4c505fe5b3c0cbc83dec6cd380e89808609c 100644 (file)
@@ -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>
 `;
index e6a6d887bd3e72b4ec9146d9f616bf542d861822..e7edd09e99f328e8b3eb529e11c4a368b69d54db 100644 (file)
  * 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>
     );
   }
index c9b3e77500e15920756f8131671be57bf7bf0708..d0350d43cc08233d4702aca30ac8388bed1c5a8a 100644 (file)
@@ -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');
 });
index e017f77860144fadece4ae11bd172ef31c974026..e0a56e3fc79f9dcee707772b3519837cb03ba2c4 100644 (file)
@@ -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 (file)
index 0000000..58c86e3
--- /dev/null
@@ -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 (file)
index 0000000..7803335
--- /dev/null
@@ -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 (file)
index 0000000..59d6155
--- /dev/null
@@ -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 (file)
index 0000000..10ea4c7
--- /dev/null
@@ -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 (file)
index 0000000..f7cfa3c
--- /dev/null
@@ -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 (file)
index 0000000..b79b4e4
--- /dev/null
@@ -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 (file)
index 0000000..9320a25
--- /dev/null
@@ -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 (file)
index 0000000..9542df4
--- /dev/null
@@ -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 (file)
index 0000000..fafb751
--- /dev/null
@@ -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 (file)
index 0000000..9f5b34b
--- /dev/null
@@ -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;
index f73428e0f40138e22c550edecf5a1458244f169c..798fce21bcc1435ea71d9b69782b76f9d3943068 100644 (file)
 .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);
 }
index b65548be61e0b00327f0cafc9706f249b850903b..5cfcff66eaeadeac68f56a490d34db41fe4784ac 100644 (file)
   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 (file)
index 0000000..3592d9b
--- /dev/null
@@ -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 (file)
index 0000000..d2090ee
--- /dev/null
@@ -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 (file)
index 0000000..6ce7483
--- /dev/null
@@ -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 (file)
index 0000000..8277e36
--- /dev/null
@@ -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 (file)
index 0000000..9e4f87c
--- /dev/null
@@ -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 (file)
index 0000000..1a072dd
--- /dev/null
@@ -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 (file)
index 0000000..d3a38a5
--- /dev/null
@@ -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>
+`;
index 881127b4524d1492fbacdee2dbeb8db39d597793..954cfa39d18bf01dc355b163bff6d18118f6d175 100644 (file)
@@ -21,6 +21,7 @@ back=Back
 backup=Backup
 backup_verb=Back up
 best=Best
+beta=BETA
 blocker=Blocker
 bold=Bold
 branch=Branch
@@ -108,6 +109,7 @@ never=Never
 new_name=New name
 none=None
 no_tags=No tags
+not_now=Not now
 off=Off
 on=On
 organization_key=Organization Key
@@ -1492,12 +1494,13 @@ my_account.create_new_organization=Create new organization
 # PROJECT PROVISIONING
 #
 #------------------------------------------------------------------------------
+provisioning.create_new_project=Create new project
 provisioning.no_analysis=No analysis has been performed since creation. The only available section is the configuration.
 provisioning.no_analysis.delete=Either you should retry analysis or simply {0}.
 provisioning.no_analysis.delete_project=delete the project
+provisioning.no_analysis_on_main_branch={branch} has not been analyzed yet.
 provisioning.only_provisioned=Only Provisioned
 provisioning.only_provisioned.tooltip=Provisioned projects are projects that have been created, but have not been analyzed yet.
-provisioning.no_analysis_on_main_branch={branch} has not been analyzed yet.
 
 
 #------------------------------------------------------------------------------
@@ -2649,23 +2652,37 @@ footer.web_api=Web API
 # ONBOARDING
 #
 #------------------------------------------------------------------------------
-onboarding.header=Welcome to SonarCloud!
-onboarding.header.description=Let us help you get started. What do you want to do?
+onboarding.header=Welcome to SonarCloud
+onboarding.header.description=Let us help you get started in your journey to code quality
+onboarding.footer=Don't worry you can do all of this later. Just click the "+" icon on your top bar.
 
 onboarding.project.header=Analyze a project
 onboarding.project.header.description=Want to quickly analyze a first project? Follow these {0} easy steps.
 
+onboarding.create_project.header=Create project(s)
+onboarding.create_project.beta_feature_x=This feature is being beta tested. We offer to create projects from your {0} repositories only for public personal projects on your personal SonarCloud organization. For other kind of projects please create them maually.
+onboarding.create_project.create_manually=Create manually
+onboarding.create_project.create_new_org=I want to create another organization
+onboarding.create_project.create_project=Create project
+onboarding.create_project.create_projects=Create projects
+onboarding.create_project.install_app_x=We need you to install the Sonarcloud {0} application in order to select which repositories you want to analyze.
+onboarding.create_project.install_app_x.button=Install SonarCloud {0} application
+onboarding.create_project.organization=Organization
+onboarding.create_project.project_key=Project key
+onboarding.create_project.project_name=Project name
+onboarding.create_project.select_repositories=Select repositories
+
 onboarding.team.header=Join a team
 onboarding.team.first_step=Well congrats, the first step is done!
 onboarding.team.how_to_join=To join a team, the only thing you need to do is to be a user registered on Sonarcloud. The administrator of the Sonarcloud organization you wish to join has to add you to his organization's members {link}. Ask him to do so!
 onboarding.team.work_in_progress=We are currently working on a better way to join a team or invite people to yours.
 
-onboarding.analyze_public_code=I want to analyze public code
-onboarding.analyze_public_code.button=Analyze a project
-onboarding.analyze_private_code=I want to analyze private code
-onboarding.analyze_private_code.button=Setup a new organization
-onboarding.contribute_existing_project=I want to contribute to an existing project
-onboarding.contribute_existing_project.button=Join a team
+onboarding.analyze_public_code.note=Free
+onboarding.analyze_public_code=Analyze public code
+onboarding.analyze_private_code=Analyze private code
+onboarding.analyze_private_code.note=From 10$ / month
+onboarding.contribute_existing_project=Join a team
+onboarding.contribute_existing_project.note=Free
 
 onboarding.token.header=Provide a token
 onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point of time in your user account.