Browse Source

SONAR-11036 Install integration with GitHub or BitBucket Cloud

* SONAR-11040 Update tutorial choices modal
* SONAR-11041 Migrate manual installation tab
* SONAR-11041 Rename button to start new project tutorial
* SONAR-11041 Rework sonarcloud tabbed page styling
* SONAR-11042 Add alm app install buttons in create project page
* Make start script compatible with ALM integration
tags/7.5
Grégoire Aubert 5 years ago
parent
commit
b08814f780
52 changed files with 1897 additions and 413 deletions
  1. 3
    1
      server/sonar-web/scripts/start.js
  2. 10
    6
      server/sonar-web/src/main/js/api/alm-integration.ts
  3. 15
    14
      server/sonar-web/src/main/js/api/components.ts
  4. 10
    6
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  5. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  6. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
  7. 10
    3
      server/sonar-web/src/main/js/app/styles/components/alerts.css
  8. 18
    0
      server/sonar-web/src/main/js/app/styles/components/modals.css
  9. 12
    0
      server/sonar-web/src/main/js/app/styles/init/forms.css
  10. 67
    0
      server/sonar-web/src/main/js/app/styles/sonarcloud.css
  11. 1
    0
      server/sonar-web/src/main/js/app/theme.js
  12. 2
    0
      server/sonar-web/src/main/js/app/types.ts
  13. 2
    6
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  14. 1
    1
      server/sonar-web/src/main/js/apps/about/routes.ts
  15. 1
    1
      server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js
  16. 3
    1
      server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
  17. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
  18. 18
    17
      server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
  19. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
  20. 1
    2
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
  21. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
  22. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
  23. 5
    3
      server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
  24. 19
    18
      server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
  25. 125
    120
      server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
  26. 4
    12
      server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css
  27. 1
    34
      server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css
  28. 8
    17
      server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx
  29. 22
    33
      server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap
  30. 46
    36
      server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
  31. 8
    9
      server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
  32. 59
    57
      server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
  33. 132
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx
  34. 182
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx
  35. 227
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx
  36. 69
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx
  37. 58
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx
  38. 79
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx
  39. 39
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
  40. 97
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap
  41. 125
    0
      server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
  42. 36
    0
      server/sonar-web/src/main/js/apps/tutorials/routes.ts
  43. 39
    1
      server/sonar-web/src/main/js/apps/tutorials/styles.css
  44. 1
    0
      server/sonar-web/src/main/js/components/controls/react-select.css
  45. 50
    0
      server/sonar-web/src/main/js/components/icons-components/OnboardingPrivateIcon.tsx
  46. 37
    0
      server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx
  47. 33
    0
      server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
  48. 68
    0
      server/sonar-web/src/main/js/components/ui/IdentityProviderLink.css
  49. 62
    0
      server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx
  50. 39
    0
      server/sonar-web/src/main/js/components/ui/__tests__/IdentityProviderLink-test.tsx
  51. 21
    0
      server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap
  52. 26
    9
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 3
- 1
server/sonar-web/scripts/start.js View 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 }
}
});


server/sonar-web/src/main/js/apps/projectsManagement/utils.ts → server/sonar-web/src/main/js/api/alm-integration.ts View File

@@ -17,10 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { SearchProjectsResponseComponent } from '../../api/components';
import { getJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export const PAGE_SIZE = 50;

export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];

export type Project = SearchProjectsResponseComponent;
export function getRepositories(): Promise<{
installation: {
installationUrl: string;
enabled: boolean;
};
}> {
return getJSON('/api/alm_integration/list_repositories').catch(throwGlobalError);
}

+ 15
- 14
server/sonar-web/src/main/js/api/components.ts View 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);
}


+ 10
- 6
server/sonar-web/src/main/js/app/components/StartupModal.tsx View 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}
/>
)}

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx View 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" />

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap View File

@@ -12,7 +12,7 @@ exports[`render 1`] = `
href="#"
onClick={[Function]}
>
my_account.analyze_new_project
provisioning.create_new_project
</a>
</li>
<li

+ 10
- 3
server/sonar-web/src/main/js/app/styles/components/alerts.css View File

@@ -60,14 +60,21 @@
color: #3c763d;
}

.alert-muted {
color: var(--disableGrayText);
border-color: var(--disableGrayBorder);
background-color: var(--disableGrayBg);
}

.alert-big {
font-size: var(--mediumFontSize);
padding: 10px 16px;
}

.page-header .alert {
clear: left;
float: left;
.alert-small {
font-size: var(--verySmallFontSize);
margin-bottom: 0;
padding: 2px 4px;
}

.page-notifs .alert {

+ 18
- 0
server/sonar-web/src/main/js/app/styles/components/modals.css View File

@@ -101,6 +101,24 @@
padding: 10px;
}

.modal-simple-head {
padding: calc(2 * var(--pagePadding)) calc(3 * var(--pagePadding));
}

.modal-simple-head h1 {
font-size: var(--hugeFontSize);
font-weight: bold;
line-height: 30px;
}

.modal-simple-body {
padding: 0 calc(3 * var(--pagePadding));
}

.modal-simple-footer {
padding: calc(2 * var(--pagePadding)) calc(3 * var(--pagePadding));
}

.modal-field,
.modal-large-field,
.modal-validation-field {

+ 12
- 0
server/sonar-web/src/main/js/app/styles/init/forms.css View 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;

+ 67
- 0
server/sonar-web/src/main/js/app/styles/sonarcloud.css View File

@@ -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);
}

+ 1
- 0
server/sonar-web/src/main/js/app/theme.js View File

@@ -64,6 +64,7 @@ module.exports = {
smallFontSize: '12px',
mediumFontSize: '14px',
bigFontSize: '16px',
hugeFontSize: '24px',

controlHeight: `${3 * grid}px`,
smallControlHeight: `${2.5 * grid}px`,

+ 2
- 0
server/sonar-web/src/main/js/app/types.ts View 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;

+ 2
- 6
server/sonar-web/src/main/js/app/utils/startReactApp.js View 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} />

+ 1
- 1
server/sonar-web/src/main/js/apps/about/routes.ts View File

@@ -27,7 +27,7 @@ const routes = [
() => (isSonarCloud() ? import('./sonarcloud/Home') : import('./components/AboutApp'))
)
},
childRoutes: isSonarCloud
childRoutes: isSonarCloud()
? [
{
path: 'contact',

+ 1
- 1
server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.js View 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();
}
}


+ 3
- 1
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx View 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"

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap View 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"

+ 18
- 17
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx View 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 = () => {

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx View File

@@ -20,11 +20,11 @@
import * as React from 'react';
import { Link } from 'react-router';
import ProjectRowActions from './ProjectRowActions';
import { Project } from './utils';
import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer';
import Checkbox from '../../components/controls/Checkbox';
import QualifierIcon from '../../components/icons-components/QualifierIcon';
import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
import { Project } from '../../api/components';

interface Props {
currentUser: { login: string };

+ 1
- 2
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx View 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';

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx View 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 };

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx View File

@@ -19,11 +19,11 @@
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Project } from './utils';
import { grantPermissionToUser } from '../../api/permissions';
import Modal from '../../components/controls/Modal';
import { SubmitButton, ResetButtonLink } from '../../components/ui/buttons';
import { translate } from '../../helpers/l10n';
import { Project } from '../../api/components';

interface Props {
currentUser: { login: string };

+ 5
- 3
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx View 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 };

+ 19
- 18
server/sonar-web/src/main/js/apps/securityReports/components/App.tsx View File

@@ -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">

+ 125
- 120
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap View 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

+ 4
- 12
server/sonar-web/src/main/js/apps/sessions/components/LoginSonarCloud.css View File

@@ -37,20 +37,12 @@
margin: var(--gridSize) auto calc(2 * var(--gridSize));
}

.sonarcloud-oauth-providers.oauth-providers > ul > li {
margin-bottom: var(--gridSize);
}

.sonarcloud-oauth-providers.oauth-providers > ul > li > a > span {
padding-left: calc(1.5 * var(--gridSize));
.sonarcloud-oauth-providers.oauth-providers > ul {
width: 174px;
}

.sonarcloud-oauth-providers.oauth-providers > ul > li > a > span::before {
content: '';
border-left: 1px var(--gray71) solid;
height: 10px;
opacity: 0.4;
margin-right: calc(1.5 * var(--gridSize));
.sonarcloud-oauth-providers.oauth-providers > ul > li {
margin-bottom: var(--gridSize);
}

.sonarcloud-oauth-providers.oauth-providers .oauth-providers-help {

+ 1
- 34
server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.css View 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;
}
@@ -28,39 +28,6 @@
margin-bottom: 30px;
}

.oauth-providers > ul > li > a {
display: block;
width: 180px;
line-height: 22px;
padding: 8px 12px;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 2px;
box-sizing: border-box;
background-color: var(--darkBlue);
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.oauth-providers > ul > li > a:hover,
.oauth-providers > ul > li > a:focus {
box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.1);
}

.oauth-providers > ul > li > a.dark-text {
color: var(--secondFontColor);
}

.oauth-providers > ul > li > a.dark-text:hover,
.oauth-providers > ul > li > a.dark-text:focus {
box-shadow: inset 0 0 0 100px rgba(0, 0, 0, 0.1);
}

.oauth-providers > ul > li > a > span {
padding-left: 6px;
}

.oauth-providers-help {
position: absolute;
top: 15px;

+ 8
- 17
server/sonar-web/src/main/js/apps/sessions/components/OAuthProviders.tsx View File

@@ -19,11 +19,11 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { translateWithParameters } from '../../../helpers/l10n';
import { IdentityProvider } from '../../../app/types';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { isDarkColor } from '../../../helpers/colors';
import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
import { translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
import { IdentityProvider } from '../../../app/types';
import './OAuthProviders.css';

interface Props {
@@ -58,25 +58,16 @@ interface ItemProps {
}

function OAuthProvider({ format, identityProvider, returnTo }: ItemProps) {
const hasDarkBackground = isDarkColor(identityProvider.backgroundColor);

return (
<li>
<a
className={classNames({ 'dark-text': !hasDarkBackground })}
href={
<IdentityProviderLink
identityProvider={identityProvider}
url={
`${getBaseUrl()}/sessions/init/${identityProvider.key}` +
`?return_to=${encodeURIComponent(returnTo)}`
}
style={{ backgroundColor: identityProvider.backgroundColor }}>
<img
alt={identityProvider.name}
height="20"
src={getBaseUrl() + identityProvider.iconPath}
width="20"
/>
}>
<span>{format(identityProvider.name)}</span>
</a>
</IdentityProviderLink>
{identityProvider.helpMessage && (
<HelpTooltip className="oauth-providers-help" overlay={identityProvider.helpMessage} />
)}

+ 22
- 33
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/OAuthProviders-test.tsx.snap View 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>
`;

+ 46
- 36
server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx View File

@@ -18,19 +18,22 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
import Modal from '../../components/controls/Modal';
import { ResetButtonLink, Button } from '../../components/ui/buttons';
import OnboardingPrivateIcon from '../../components/icons-components/OnboardingPrivateIcon';
import OnboardingProjectIcon from '../../components/icons-components/OnboardingProjectIcon';
import OnboardingTeamIcon from '../../components/icons-components/OnboardingTeamIcon';
import { Button, ResetButtonLink } from '../../components/ui/buttons';
import { translate } from '../../helpers/l10n';
import { CurrentUser, isLoggedIn } from '../../app/types';
import { getCurrentUser } from '../../store/rootReducer';
import './styles.css';

interface OwnProps {
onFinish: () => void;
onClose: (doSkipOnboarding?: boolean) => void;
onOpenOrganizationOnboarding: () => void;
onOpenProjectOnboarding: () => void;
onOpenTeamOnboarding: () => void;
}

@@ -41,12 +44,25 @@ interface StateProps {
type Props = OwnProps & StateProps;

export class Onboarding extends React.PureComponent<Props> {
static contextTypes = {
router: PropTypes.object
};

componentDidMount() {
if (!isLoggedIn(this.props.currentUser)) {
handleRequiredAuthentication();
}
}

openProjectOnboarding = () => {
this.props.onClose(false);
this.context.router.push('/onboarding');
};

onFinish = () => {
this.props.onClose(true);
};

render() {
if (!isLoggedIn(this.props.currentUser)) {
return null;
@@ -57,41 +73,35 @@ export class Onboarding extends React.PureComponent<Props> {
<Modal
contentLabel={header}
medium={true}
onRequestClose={this.props.onFinish}
onRequestClose={this.onFinish}
shouldCloseOnOverlayClick={false}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body">
<p className="spacer-top big-spacer-bottom">
{translate('onboarding.header.description')}
</p>
<ul className="onboarding-choices">
<li className="text-center">
<p className="big-spacer-bottom">{translate('onboarding.analyze_public_code')}</p>
<Button onClick={this.props.onOpenProjectOnboarding}>
{translate('onboarding.analyze_public_code.button')}
</Button>
</li>
<li className="text-center">
<p className="big-spacer-bottom">{translate('onboarding.analyze_private_code')}</p>
<Button onClick={this.props.onOpenOrganizationOnboarding}>
{translate('onboarding.analyze_private_code.button')}
</Button>
</li>
<li className="text-center">
<p className="big-spacer-bottom">
{translate('onboarding.contribute_existing_project')}
</p>
<Button onClick={this.props.onOpenTeamOnboarding}>
{translate('onboarding.contribute_existing_project.button')}
</Button>
</li>
</ul>
<div className="modal-simple-head text-center">
<h1>{translate('onboarding.header')}</h1>
<p className="spacer-top">{translate('onboarding.header.description')}</p>
</div>
<div className="modal-simple-body text-center onboarding-choices">
<Button className="onboarding-choice" onClick={this.openProjectOnboarding}>
<OnboardingProjectIcon />
<span>{translate('onboarding.analyze_public_code')}</span>
<p className="note">{translate('onboarding.analyze_public_code.note')}</p>
</Button>
<Button className="onboarding-choice" onClick={this.props.onOpenOrganizationOnboarding}>
<OnboardingPrivateIcon />
<span>{translate('onboarding.analyze_private_code')}</span>
<p className="note">{translate('onboarding.analyze_private_code.note')}</p>
</Button>
<Button className="onboarding-choice" onClick={this.props.onOpenTeamOnboarding}>
<OnboardingTeamIcon />
<span>{translate('onboarding.contribute_existing_project')}</span>
<p className="note">{translate('onboarding.contribute_existing_project.note')}</p>
</Button>
</div>
<div className="modal-simple-footer text-center">
<ResetButtonLink className="spacer-bottom" onClick={this.onFinish}>
{translate('not_now')}
</ResetButtonLink>
<p className="note">{translate('onboarding.footer')}</p>
</div>
<footer className="modal-foot">
<ResetButtonLink onClick={this.props.onFinish}>{translate('close')}</ResetButtonLink>
</footer>
</Modal>
);
}

+ 8
- 9
server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx View 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');
});

+ 59
- 57
server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap View 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>
`;

+ 132
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx View File

@@ -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>
)}
</>
);
}
}

+ 182
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx View File

@@ -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
);

+ 227
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/ManualProjectCreate.tsx View File

@@ -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
);

+ 69
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx View File

@@ -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} />);
}

+ 58
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/CreateProjectOnboarding-test.tsx View File

@@ -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}
/>
);
}

+ 79
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/ManualProjectCreate-test.tsx View File

@@ -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}
/>
);
}

+ 39
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap View File

@@ -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>
`;

+ 97
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap View File

@@ -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>
`;

+ 125
- 0
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap View File

@@ -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>
`;

+ 36
- 0
server/sonar-web/src/main/js/apps/tutorials/routes.ts View File

@@ -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;

+ 39
- 1
server/sonar-web/src/main/js/apps/tutorials/styles.css View File

@@ -58,5 +58,43 @@
.onboarding-choices {
display: flex;
justify-content: space-around;
padding: 24px 0 44px;
padding-top: 44px;
padding-bottom: 44px;
background-color: var(--barBackgroundColor);
}

.onboarding-choice {
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: calc(2 * var(--gridSize));
width: 190px;
height: 190px;
background-color: #fff;
border: solid 1px #fff;
border-radius: 3px;
transition: all 0.2s ease;
box-shadow: 0 1px 1px 1px var(--barBorderColor);
}

.onboarding-choice svg {
color: var(--gray40);
margin-bottom: calc(3 * var(--gridSize));
}

.onboarding-choice span {
font-size: var(--mediumFontSize);
margin-bottom: calc(var(--gridSize) / 2);
}

.onboarding-choice .note {
font-weight: 400;
}

.onboarding-choice:hover,
.onboarding-choice:focus,
.onboarding-choice:active {
background-color: #fff;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.35);
color: var(--darkBlue);
}

+ 1
- 0
server/sonar-web/src/main/js/components/controls/react-select.css View File

@@ -172,6 +172,7 @@
margin: 0;
outline: none;
padding: 0;
box-shadow: none;
-webkit-appearance: none;
}


+ 50
- 0
server/sonar-web/src/main/js/components/icons-components/OnboardingPrivateIcon.tsx View File

@@ -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>
);
}

+ 37
- 0
server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx View File

@@ -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>
);
}

+ 33
- 0
server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx View File

@@ -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>
);
}

+ 68
- 0
server/sonar-web/src/main/js/components/ui/IdentityProviderLink.css View File

@@ -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));
}

+ 62
- 0
server/sonar-web/src/main/js/components/ui/IdentityProviderLink.tsx View File

@@ -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>
);
}

+ 39
- 0
server/sonar-web/src/main/js/components/ui/__tests__/IdentityProviderLink-test.tsx View File

@@ -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();
});

+ 21
- 0
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap View File

@@ -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>
`;

+ 26
- 9
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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.

Loading…
Cancel
Save