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