* Move/Rename files of tutorials folder * SONAR-11049 Update tutorial UI and move it inside the project dashboard * SONAR-11050 Update tutorial to skip now useless steps * Remove unused style * SONAR-11030 Make dashboard tutorial work with already known project key * Better manage error messages when no analysis and analyzed branches * SONAR-11052 Refresh project status as long as there is no analysis * SONAR-11051 Add infos suggestions depending on the ALM of the project * Do no display tutorial when there is analyses in the pipetags/7.5
@@ -24,7 +24,7 @@ | |||
.alert-info { | |||
border-color: #bce8f1; | |||
background-color: #d9edf7; | |||
color: #31708f; | |||
color: #666666; | |||
} | |||
.alert-success { |
@@ -176,7 +176,12 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
differenceBy(newTasksInProgress, tasksInProgress, 'id').length > 0); | |||
shouldFetchComponent = Boolean(currentTaskChanged || progressChanged); | |||
if (!shouldFetchComponent && component && newTasksInProgress.length > 0) { | |||
if ( | |||
!shouldFetchComponent && | |||
component && | |||
(newTasksInProgress.length > 0 || !component.analysisDate) | |||
) { | |||
// Refresh the status as long as there is tasks in progress or no analysis | |||
window.clearTimeout(this.watchStatusTimer); | |||
this.watchStatusTimer = window.setTimeout( | |||
() => this.fetchStatus(component), |
@@ -35,7 +35,7 @@ import { lazyLoad } from '../../components/lazyLoad'; | |||
const CreateOrganizationForm = lazyLoad(() => | |||
import('../../apps/account/organizations/CreateOrganizationForm') | |||
); | |||
const Onboarding = lazyLoad(() => import('../../apps/tutorials/Onboarding')); | |||
const OnboardingModal = lazyLoad(() => import('../../apps/tutorials/onboarding/OnboardingModal')); | |||
const LicensePromptModal = lazyLoad( | |||
() => import('../../apps/marketplace/components/LicensePromptModal'), | |||
'LicensePromptModal' | |||
@@ -135,7 +135,7 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
openProjectOnboarding = () => { | |||
if (isSonarCloud()) { | |||
this.setState({ automatic: false, modal: undefined }); | |||
this.context.router.push(`/onboarding`); | |||
this.context.router.push(`/projects/create`); | |||
} else { | |||
this.setState({ modal: ModalKey.projectOnboarding }); | |||
} | |||
@@ -189,7 +189,7 @@ export class StartupModal extends React.PureComponent<Props, State> { | |||
{this.props.children} | |||
{modal === ModalKey.license && <LicensePromptModal onClose={this.closeLicense} />} | |||
{modal === ModalKey.onboarding && ( | |||
<Onboarding | |||
<OnboardingModal | |||
onClose={this.closeOnboarding} | |||
onOpenOrganizationOnboarding={this.openOrganizationOnboarding} | |||
onOpenProjectOnboarding={this.openProjectOnboarding} |
@@ -36,25 +36,23 @@ import { STATUSES } from '../../../apps/background-tasks/constants'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
jest.mock('../../../api/branches', () => ({ | |||
getBranches: jest.fn(() => Promise.resolve([])), | |||
getPullRequests: jest.fn(() => Promise.resolve([])) | |||
getBranches: jest.fn().mockResolvedValue([]), | |||
getPullRequests: jest.fn().mockResolvedValue([]) | |||
})); | |||
jest.mock('../../../api/ce', () => ({ | |||
getTasksForComponent: jest.fn(() => Promise.resolve({ queue: [] })) | |||
getTasksForComponent: jest.fn().mockResolvedValue({ queue: [] }) | |||
})); | |||
jest.mock('../../../api/components', () => ({ | |||
getComponentData: jest.fn(() => Promise.resolve({})) | |||
getComponentData: jest.fn().mockResolvedValue({ analysisDate: '2018-07-30' }) | |||
})); | |||
jest.mock('../../../api/nav', () => ({ | |||
getComponentNavigation: jest.fn(() => | |||
Promise.resolve({ | |||
breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }], | |||
key: 'portfolioKey' | |||
}) | |||
) | |||
getComponentNavigation: jest.fn().mockResolvedValue({ | |||
breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }], | |||
key: 'portfolioKey' | |||
}) | |||
})); | |||
// mock this, because some of its children are using redux store | |||
@@ -90,14 +88,12 @@ it('changes component', () => { | |||
}); | |||
it("loads branches for module's project", async () => { | |||
(getComponentNavigation as jest.Mock<any>).mockImplementationOnce(() => | |||
Promise.resolve({ | |||
breadcrumbs: [ | |||
{ key: 'projectKey', name: 'project', qualifier: 'TRK' }, | |||
{ key: 'moduleKey', name: 'module', qualifier: 'BRC' } | |||
] | |||
}) | |||
); | |||
(getComponentNavigation as jest.Mock<any>).mockResolvedValueOnce({ | |||
breadcrumbs: [ | |||
{ key: 'projectKey', name: 'project', qualifier: 'TRK' }, | |||
{ key: 'moduleKey', name: 'module', qualifier: 'BRC' } | |||
] | |||
}); | |||
mount( | |||
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'moduleKey' } }}> | |||
@@ -149,9 +145,7 @@ it('updates branches on change', () => { | |||
}); | |||
it('loads organization', async () => { | |||
(getComponentData as jest.Mock<any>).mockImplementationOnce(() => | |||
Promise.resolve({ organization: 'org' }) | |||
); | |||
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ organization: 'org' }); | |||
const fetchOrganizations = jest.fn(); | |||
mount( | |||
@@ -166,9 +160,7 @@ it('loads organization', async () => { | |||
}); | |||
it('fetches status', async () => { | |||
(getComponentData as jest.Mock<any>).mockImplementationOnce(() => | |||
Promise.resolve({ organization: 'org' }) | |||
); | |||
(getComponentData as jest.Mock<any>).mockResolvedValueOnce({ organization: 'org' }); | |||
mount( | |||
<ComponentContainer fetchOrganizations={jest.fn()} location={{ query: { id: 'foo' } }}> |
@@ -51,7 +51,7 @@ | |||
.alert-info { | |||
border-color: #bce8f1; | |||
background-color: #d9edf7; | |||
color: #31708f; | |||
color: #666666; | |||
} | |||
.alert-success { |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
.boxed-group { | |||
margin-bottom: 20px; | |||
margin-bottom: calc(2.5 * var(--gridSize)); | |||
border: 1px solid var(--barBorderColor); | |||
border-radius: 2px; | |||
background-color: #fff; |
@@ -187,7 +187,6 @@ | |||
} | |||
.page-sidebar-fixed { | |||
width: 30%; | |||
min-width: 300px; | |||
flex-shrink: 0; | |||
padding-left: 40px; | |||
@@ -228,17 +227,6 @@ | |||
} | |||
} | |||
.page-sidebar-sticky .page-sidebar-sticky-inner .search-navigator-facets-list { | |||
width: 260px; | |||
margin-left: calc(50vw - 640px + 290px - 260px - 37px); | |||
} | |||
@media (max-width: 1335px) { | |||
.page-sidebar-sticky .page-sidebar-sticky-inner .search-navigator-facets-list { | |||
margin-left: 20px; | |||
} | |||
} | |||
.layout-page { | |||
display: flex; | |||
align-items: stretch; |
@@ -293,6 +293,11 @@ td.big-spacer-top { | |||
flex-direction: row; | |||
} | |||
.display-flex-column { | |||
display: flex !important; | |||
flex-direction: column; | |||
} | |||
.display-flex-center { | |||
display: flex !important; | |||
align-items: center; |
@@ -37,19 +37,22 @@ | |||
display: flex; | |||
clear: left; | |||
margin-bottom: calc(3 * var(--gridSize)); | |||
box-shadow: 0 1px 0 var(--barBorderColor); | |||
border-bottom: 1px solid var(--barBorderColor); | |||
font-size: var(--mediumFontSize); | |||
} | |||
.sonarcloud .flex-tabs > li > a { | |||
position: relative; | |||
display: block; | |||
top: 1px; | |||
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; | |||
padding-bottom: calc(1.5 * var(--gridSize)); | |||
border-bottom: 3px solid transparent; | |||
transition: color 0.2s ease; | |||
} | |||
@@ -61,6 +61,8 @@ export interface Breadcrumb { | |||
} | |||
export interface Component extends LightComponent { | |||
almId?: string; | |||
almRepoUrl?: string; | |||
analysisDate?: string; | |||
breadcrumbs: Breadcrumb[]; | |||
configuration?: ComponentConfiguration; |
@@ -57,7 +57,7 @@ export default function FacetsList(props: Props) { | |||
props.selectedProfile === undefined || | |||
!props.query.activation; | |||
return ( | |||
<div className="search-navigator-facets-list"> | |||
<> | |||
<LanguageFacet | |||
onChange={props.onFilterChange} | |||
onToggle={props.onFacetToggle} | |||
@@ -145,6 +145,6 @@ export default function FacetsList(props: Props) { | |||
/> | |||
</> | |||
)} | |||
</div> | |||
</> | |||
); | |||
} |
@@ -77,7 +77,7 @@ export default class Sidebar extends React.PureComponent { | |||
render() { | |||
return ( | |||
<div className="search-navigator-facets-list"> | |||
<div> | |||
<ProjectOverviewFacet | |||
onChange={this.changeMetric} | |||
selected={this.props.selectedMetric} |
@@ -1,9 +1,7 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display two facets 1`] = ` | |||
<div | |||
className="search-navigator-facets-list" | |||
> | |||
<div> | |||
<ProjectOverviewFacet | |||
onChange={[Function]} | |||
selected="duplicated_lines_density" |
@@ -77,7 +77,7 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
(this.props.organization && this.props.organization.key); | |||
return ( | |||
<div className="search-navigator-facets-list"> | |||
<> | |||
<TypeFacet | |||
fetching={this.props.loadingFacets.types === true} | |||
loading={this.props.loading} | |||
@@ -256,7 +256,7 @@ export default class Sidebar extends React.PureComponent<Props> { | |||
stats={facets.authors} | |||
/> | |||
)} | |||
</div> | |||
</> | |||
); | |||
} | |||
} |
@@ -19,11 +19,21 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import OverviewApp from './OverviewApp'; | |||
import { Helmet } from 'react-helmet'; | |||
import EmptyOverview from './EmptyOverview'; | |||
import OverviewApp from './OverviewApp'; | |||
import SonarCloudEmptyOverview from './SonarCloudEmptyOverview'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { Component, BranchLike } from '../../../app/types'; | |||
import { isShortLivingBranch } from '../../../helpers/branches'; | |||
import { getShortLivingBranchUrl, getCodeUrl } from '../../../helpers/urls'; | |||
import { | |||
getShortLivingBranchUrl, | |||
getCodeUrl, | |||
getProjectUrl, | |||
getBaseUrl, | |||
getPathUrlAsString | |||
} from '../../../helpers/urls'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
@@ -67,22 +77,43 @@ export default class App extends React.PureComponent<Props> { | |||
return null; | |||
} | |||
if (!component.analysisDate) { | |||
return ( | |||
<EmptyOverview | |||
component={component.key} | |||
hasBranches={branchLikes.length > 1} | |||
showWarning={!this.props.isPending && !this.props.isInProgress} | |||
/> | |||
); | |||
} | |||
return ( | |||
<OverviewApp | |||
branchLike={branchLike} | |||
component={component} | |||
onComponentChange={this.props.onComponentChange} | |||
/> | |||
<> | |||
{isSonarCloud() && ( | |||
<Helmet> | |||
<link | |||
href={getBaseUrl() + getPathUrlAsString(getProjectUrl(component.key))} | |||
rel="canonical" | |||
/> | |||
</Helmet> | |||
)} | |||
<Suggestions suggestions="overview" /> | |||
{!component.analysisDate && | |||
(isSonarCloud() ? ( | |||
<SonarCloudEmptyOverview | |||
branchLike={branchLike} | |||
branchLikes={branchLikes} | |||
component={component} | |||
hasAnalyses={this.props.isPending || this.props.isInProgress} | |||
onComponentChange={this.props.onComponentChange} | |||
/> | |||
) : ( | |||
<EmptyOverview | |||
branchLike={branchLike} | |||
branchLikes={branchLikes} | |||
component={component.key} | |||
showWarning={!this.props.isPending && !this.props.isInProgress} | |||
/> | |||
))} | |||
{component.analysisDate && ( | |||
<OverviewApp | |||
branchLike={branchLike} | |||
component={component} | |||
onComponentChange={this.props.onComponentChange} | |||
/> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -21,30 +21,39 @@ import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { BranchLike } from '../../../app/types'; | |||
import { isBranch, isLongLivingBranch } from '../../../helpers/branches'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
branchLikes: BranchLike[]; | |||
component: string; | |||
hasBranches?: boolean; | |||
showWarning?: boolean; | |||
} | |||
export default function EmptyOverview({ component, hasBranches, showWarning }: Props) { | |||
const rawMessage = translate('provisioning.no_analysis.delete'); | |||
const head = rawMessage.substr(0, rawMessage.indexOf('{0}')); | |||
const tail = rawMessage.substr(rawMessage.indexOf('{0}') + 3); | |||
export default function EmptyOverview({ branchLike, branchLikes, component, showWarning }: Props) { | |||
const hasBranches = branchLikes.length > 1; | |||
const hasBadConfig = | |||
branchLikes.length > 2 || | |||
(branchLikes.length === 2 && branchLikes.some(branch => isLongLivingBranch(branch))); | |||
const branchWarnMsg = hasBadConfig | |||
? translate('provisioning.no_analysis_on_main_branch.bad_configuration') | |||
: translate('provisioning.no_analysis_on_main_branch'); | |||
return ( | |||
<div className="page page-limited"> | |||
{showWarning && ( | |||
<div className="big-spacer-bottom"> | |||
<div className="alert alert-warning"> | |||
{hasBranches ? ( | |||
{hasBranches && isBranch(branchLike) ? ( | |||
<FormattedMessage | |||
defaultMessage={translate('provisioning.no_analysis_on_main_branch')} | |||
id="provisioning.no_analysis_on_main_branch" | |||
defaultMessage={branchWarnMsg} | |||
id={branchWarnMsg} | |||
values={{ | |||
branch: ( | |||
<div className="outline-badge text-baseline little-spacer-right"> | |||
branchName: branchLike.name, | |||
branchType: ( | |||
<div className="outline-badge text-baseline"> | |||
{translate('branches.main_branch')} | |||
</div> | |||
) | |||
@@ -57,13 +66,19 @@ export default function EmptyOverview({ component, hasBranches, showWarning }: P | |||
{!hasBranches && ( | |||
<div className="big-spacer-top"> | |||
{head} | |||
<Link | |||
className="text-danger" | |||
to={{ pathname: '/project/deletion', query: { id: component } }}> | |||
{translate('provisioning.no_analysis.delete_project')} | |||
</Link> | |||
{tail} | |||
<FormattedMessage | |||
defaultMessage={translate('provisioning.no_analysis.delete')} | |||
id={'provisioning.no_analysis.delete'} | |||
values={{ | |||
link: ( | |||
<Link | |||
className="text-danger" | |||
to={{ pathname: '/project/deletion', query: { id: component } }}> | |||
{translate('provisioning.no_analysis.delete_project')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</div> | |||
)} | |||
</div> |
@@ -20,16 +20,14 @@ | |||
import * as React from 'react'; | |||
import { uniq } from 'lodash'; | |||
import { connect } from 'react-redux'; | |||
import { Helmet } from 'react-helmet'; | |||
import QualityGate from '../qualityGate/QualityGate'; | |||
import ApplicationQualityGate from '../qualityGate/ApplicationQualityGate'; | |||
import BugsAndVulnerabilities from '../main/BugsAndVulnerabilities'; | |||
import CodeSmells from '../main/CodeSmells'; | |||
import Coverage from '../main/Coverage'; | |||
import Duplications from '../main/Duplications'; | |||
import MetaContainer from '../meta/MetaContainer'; | |||
import QualityGate from '../qualityGate/QualityGate'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { getMeasuresAndMeta } from '../../../api/measures'; | |||
import { getAllTimeMachineData, History } from '../../../api/time-machine'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
@@ -52,8 +50,6 @@ import { fetchMetrics } from '../../../store/rootActions'; | |||
import { getMetrics } from '../../../store/rootReducer'; | |||
import { BranchLike, Component, Metric } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getProjectUrl, getSonarCloudUrlAsString } from '../../../helpers/urls'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
import '../styles.css'; | |||
interface OwnProps { | |||
@@ -246,14 +242,6 @@ export class OverviewApp extends React.PureComponent<Props, State> { | |||
return ( | |||
<div className="page page-limited"> | |||
<div className="overview page-with-sidebar"> | |||
<Suggestions suggestions="overview" /> | |||
{isSonarCloud() && ( | |||
<Helmet> | |||
<link href={getSonarCloudUrlAsString(getProjectUrl(component.key))} rel="canonical" /> | |||
</Helmet> | |||
)} | |||
{this.renderMain()} | |||
<div className="overview-sidebar page-sidebar-fixed"> |
@@ -0,0 +1,126 @@ | |||
/* | |||
* 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 { connect } from 'react-redux'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import AnalyzeTutorial from '../../tutorials/analyzeProject/AnalyzeTutorial'; | |||
import MetaContainer from '../meta/MetaContainer'; | |||
import { BranchLike, Component, CurrentUser, isLoggedIn } from '../../../app/types'; | |||
import { isLongLivingBranch, isBranch, isMainBranch } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import '../../../app/styles/sonarcloud.css'; | |||
interface OwnProps { | |||
branchLike?: BranchLike; | |||
branchLikes: BranchLike[]; | |||
component: Component; | |||
hasAnalyses?: boolean; | |||
onComponentChange: (changes: {}) => void; | |||
} | |||
interface StateProps { | |||
currentUser: CurrentUser; | |||
} | |||
type Props = OwnProps & StateProps; | |||
export function SonarCloudEmptyOverview({ | |||
branchLike, | |||
branchLikes, | |||
component, | |||
currentUser, | |||
hasAnalyses, | |||
onComponentChange | |||
}: Props) { | |||
const hasBranches = branchLikes.length > 1; | |||
const hasBadBranchConfig = | |||
branchLikes.length > 2 || | |||
(branchLikes.length === 2 && branchLikes.some(branch => isLongLivingBranch(branch))); | |||
return ( | |||
<div className="page page-limited"> | |||
<div className="overview page-with-sidebar"> | |||
<div className="overview-main page-main sonarcloud"> | |||
{isLoggedIn(currentUser) && isMainBranch(branchLike) ? ( | |||
<> | |||
{hasBranches && ( | |||
<WarningMessage | |||
branchLike={branchLike} | |||
message={ | |||
hasBadBranchConfig | |||
? translate('provisioning.no_analysis_on_main_branch.bad_configuration') | |||
: translate('provisioning.no_analysis_on_main_branch') | |||
} | |||
/> | |||
)} | |||
{!hasBranches && | |||
!hasAnalyses && <AnalyzeTutorial component={component} currentUser={currentUser} />} | |||
</> | |||
) : ( | |||
<WarningMessage | |||
branchLike={branchLike} | |||
message={translate('provisioning.no_analysis_on_main_branch')} | |||
/> | |||
)} | |||
</div> | |||
<div className="overview-sidebar page-sidebar-fixed"> | |||
<MetaContainer | |||
branchLike={branchLike} | |||
component={component} | |||
onComponentChange={onComponentChange} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
export function WarningMessage({ | |||
branchLike, | |||
message | |||
}: { | |||
branchLike?: BranchLike; | |||
message: string; | |||
}) { | |||
if (!isBranch(branchLike)) { | |||
return null; | |||
} | |||
return ( | |||
<div className="alert alert-warning"> | |||
<FormattedMessage | |||
defaultMessage={message} | |||
id={message} | |||
values={{ | |||
branchName: branchLike.name, | |||
branchType: ( | |||
<div className="outline-badge text-baseline">{translate('branches.main_branch')}</div> | |||
) | |||
}} | |||
/> | |||
</div> | |||
); | |||
} | |||
const mapStateToProps = (state: any) => ({ | |||
currentUser: getCurrentUser(state) | |||
}); | |||
export default connect<StateProps, {}, OwnProps>(mapStateToProps)(SonarCloudEmptyOverview); |
@@ -20,9 +20,10 @@ | |||
import * as React from 'react'; | |||
import { mount, shallow } from 'enzyme'; | |||
import App from '../App'; | |||
import OverviewApp from '../OverviewApp'; | |||
import EmptyOverview from '../EmptyOverview'; | |||
import { BranchType, LongLivingBranch } from '../../../../app/types'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | |||
const component = { | |||
key: 'foo', | |||
@@ -34,13 +35,34 @@ const component = { | |||
version: '0.0.1' | |||
}; | |||
beforeEach(() => { | |||
(isSonarCloud as jest.Mock<any>).mockClear(); | |||
(isSonarCloud as jest.Mock<any>).mockReturnValue(false); | |||
}); | |||
it('should render OverviewApp', () => { | |||
expect(getWrapper().type()).toBe(OverviewApp); | |||
expect( | |||
getWrapper() | |||
.find('Connect(OverviewApp)') | |||
.exists() | |||
).toBeTruthy(); | |||
}); | |||
it('should render EmptyOverview', () => { | |||
const output = getWrapper({ component: { key: 'foo' } }); | |||
expect(output.type()).toBe(EmptyOverview); | |||
expect( | |||
getWrapper({ component: { key: 'foo' } }) | |||
.find('EmptyOverview') | |||
.exists() | |||
).toBeTruthy(); | |||
}); | |||
it('should render SonarCloudEmptyOverview', () => { | |||
(isSonarCloud as jest.Mock<any>).mockReturnValue(true); | |||
expect( | |||
getWrapper({ component: { key: 'foo' } }) | |||
.find('Connect(SonarCloudEmptyOverview)') | |||
.exists() | |||
).toBeTruthy(); | |||
}); | |||
it('redirects on Code page for files', () => { |
@@ -20,17 +20,41 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import EmptyOverview from '../EmptyOverview'; | |||
import { BranchType } from '../../../../app/types'; | |||
const branch = { isMain: true, name: 'b', type: BranchType.LONG }; | |||
it('renders', () => { | |||
expect(shallow(<EmptyOverview component="abcd" showWarning={true} />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<EmptyOverview branchLikes={[]} component="abcd" showWarning={true} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('does not render warning', () => { | |||
expect(shallow(<EmptyOverview component="abcd" showWarning={false} />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<EmptyOverview branchLikes={[]} component="abcd" showWarning={false} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render another message when there are branches', () => { | |||
expect( | |||
shallow(<EmptyOverview component="abcd" hasBranches={true} showWarning={true} />) | |||
shallow( | |||
<EmptyOverview | |||
branchLike={branch} | |||
branchLikes={[branch, branch]} | |||
component="abcd" | |||
showWarning={true} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<EmptyOverview | |||
branchLike={branch} | |||
branchLikes={[branch, branch, branch]} | |||
component="abcd" | |||
showWarning={true} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,117 @@ | |||
/* | |||
* 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 { SonarCloudEmptyOverview, WarningMessage } from '../SonarCloudEmptyOverview'; | |||
import { BranchType } from '../../../../app/types'; | |||
const branch = { isMain: true, name: 'b', type: BranchType.LONG }; | |||
const component = { | |||
key: 'foo', | |||
analysisDate: '2016-01-01', | |||
breadcrumbs: [], | |||
name: 'Foo', | |||
organization: 'org', | |||
qualifier: 'TRK', | |||
version: '0.0.1' | |||
}; | |||
const LoggedInUser = { | |||
isLoggedIn: true, | |||
login: 'luke', | |||
name: 'Skywalker' | |||
}; | |||
it('renders correctly', () => { | |||
expect( | |||
shallow( | |||
<SonarCloudEmptyOverview | |||
branchLike={branch} | |||
branchLikes={[branch]} | |||
component={component} | |||
currentUser={LoggedInUser} | |||
onComponentChange={jest.fn()} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render another message when there are branches', () => { | |||
expect( | |||
shallow( | |||
<SonarCloudEmptyOverview | |||
branchLike={branch} | |||
branchLikes={[branch, branch]} | |||
component={component} | |||
currentUser={LoggedInUser} | |||
onComponentChange={jest.fn()} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<SonarCloudEmptyOverview | |||
branchLike={branch} | |||
branchLikes={[branch, branch, branch]} | |||
component={component} | |||
currentUser={LoggedInUser} | |||
onComponentChange={jest.fn()} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should not render the tutorial', () => { | |||
expect( | |||
shallow( | |||
<SonarCloudEmptyOverview | |||
branchLike={branch} | |||
branchLikes={[branch]} | |||
component={component} | |||
currentUser={LoggedInUser} | |||
hasAnalyses={true} | |||
onComponentChange={jest.fn()} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render warning message', () => { | |||
expect(shallow(<WarningMessage branchLike={branch} message="foo" />)).toMatchSnapshot(); | |||
}); | |||
it('should not render warning message', () => { | |||
expect( | |||
shallow( | |||
<WarningMessage | |||
branchLike={{ | |||
base: 'foo', | |||
branch: 'bar', | |||
key: '1', | |||
title: 'PR bar' | |||
}} | |||
message="foo" | |||
/> | |||
) | |||
.find('FormattedMessage') | |||
.exists() | |||
).toBeFalsy(); | |||
}); |
@@ -1,51 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import Timeline from '../Timeline'; | |||
import { parseDate } from '../../../../helpers/dates'; | |||
const range = parseDate('2017-05-01T00:00:00.000Z'); | |||
const history = [ | |||
{ date: parseDate('2017-04-08T00:00:00.000Z'), value: '29.6' }, | |||
{ date: parseDate('2017-04-09T00:00:00.000Z'), value: '170.8' }, | |||
{ date: parseDate('2017-05-08T00:00:00.000Z'), value: '360' }, | |||
{ date: parseDate('2017-05-09T00:00:00.000Z'), value: '39' } | |||
]; | |||
it('should render correctly with an "after" range', () => { | |||
expect(shallow(<Timeline after={range} history={history} />)).toMatchSnapshot(); | |||
}); | |||
it('should render correctly with a "before" range', () => { | |||
expect(shallow(<Timeline before={range} history={history} />)).toMatchSnapshot(); | |||
}); | |||
it('should have a correct domain with strings or numbers', () => { | |||
const date = parseDate('2017-05-08T00:00:00.000Z'); | |||
const wrapper = shallow(<Timeline after={range} history={history} />); | |||
expect(wrapper.find('LineChart').props().domain).toEqual([0, 360]); | |||
wrapper.setProps({ history: [{ date, value: '360.33' }, { date, value: '39.54' }] }); | |||
expect(wrapper.find('LineChart').props().domain).toEqual([0, 360.33]); | |||
wrapper.setProps({ history: [{ date, value: 360 }, { date, value: 39 }] }); | |||
expect(wrapper.find('LineChart').props().domain).toEqual([0, 360]); | |||
}); |
@@ -30,22 +30,29 @@ exports[`renders 1`] = ` | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Link | |||
className="text-danger" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
<FormattedMessage | |||
defaultMessage="provisioning.no_analysis.delete" | |||
id="provisioning.no_analysis.delete" | |||
values={ | |||
Object { | |||
"pathname": "/project/deletion", | |||
"query": Object { | |||
"id": "abcd", | |||
}, | |||
"link": <Link | |||
className="text-danger" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/deletion", | |||
"query": Object { | |||
"id": "abcd", | |||
}, | |||
} | |||
} | |||
> | |||
provisioning.no_analysis.delete_project | |||
</Link>, | |||
} | |||
} | |||
> | |||
provisioning.no_analysis.delete_project | |||
</Link> | |||
ovisioning.no_analysis.delete | |||
/> | |||
</div> | |||
</div> | |||
<div> | |||
@@ -74,8 +81,46 @@ exports[`should render another message when there are branches 1`] = ` | |||
id="provisioning.no_analysis_on_main_branch" | |||
values={ | |||
Object { | |||
"branch": <div | |||
className="outline-badge text-baseline little-spacer-right" | |||
"branchName": "b", | |||
"branchType": <div | |||
className="outline-badge text-baseline" | |||
> | |||
branches.main_branch | |||
</div>, | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> | |||
<div> | |||
<h4> | |||
key | |||
</h4> | |||
<code> | |||
abcd | |||
</code> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render another message when there are branches 2`] = ` | |||
<div | |||
className="page page-limited" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<div | |||
className="alert alert-warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="provisioning.no_analysis_on_main_branch.bad_configuration" | |||
id="provisioning.no_analysis_on_main_branch.bad_configuration" | |||
values={ | |||
Object { | |||
"branchName": "b", | |||
"branchType": <div | |||
className="outline-badge text-baseline" | |||
> | |||
branches.main_branch | |||
</div>, |
@@ -0,0 +1,229 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders correctly 1`] = ` | |||
<div | |||
className="page page-limited" | |||
> | |||
<div | |||
className="overview page-with-sidebar" | |||
> | |||
<div | |||
className="overview-main page-main sonarcloud" | |||
> | |||
<React.Fragment> | |||
<AnalyzeTutorial | |||
component={ | |||
Object { | |||
"analysisDate": "2016-01-01", | |||
"breadcrumbs": Array [], | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
"version": "0.0.1", | |||
} | |||
} | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
"login": "luke", | |||
"name": "Skywalker", | |||
} | |||
} | |||
/> | |||
</React.Fragment> | |||
</div> | |||
<div | |||
className="overview-sidebar page-sidebar-fixed" | |||
> | |||
<Connect(Meta) | |||
branchLike={ | |||
Object { | |||
"isMain": true, | |||
"name": "b", | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"analysisDate": "2016-01-01", | |||
"breadcrumbs": Array [], | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
"version": "0.0.1", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should not render the tutorial 1`] = ` | |||
<div | |||
className="page page-limited" | |||
> | |||
<div | |||
className="overview page-with-sidebar" | |||
> | |||
<div | |||
className="overview-main page-main sonarcloud" | |||
> | |||
<React.Fragment /> | |||
</div> | |||
<div | |||
className="overview-sidebar page-sidebar-fixed" | |||
> | |||
<Connect(Meta) | |||
branchLike={ | |||
Object { | |||
"isMain": true, | |||
"name": "b", | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"analysisDate": "2016-01-01", | |||
"breadcrumbs": Array [], | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
"version": "0.0.1", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render another message when there are branches 1`] = ` | |||
<div | |||
className="page page-limited" | |||
> | |||
<div | |||
className="overview page-with-sidebar" | |||
> | |||
<div | |||
className="overview-main page-main sonarcloud" | |||
> | |||
<React.Fragment> | |||
<WarningMessage | |||
branchLike={ | |||
Object { | |||
"isMain": true, | |||
"name": "b", | |||
"type": "LONG", | |||
} | |||
} | |||
message="provisioning.no_analysis_on_main_branch" | |||
/> | |||
</React.Fragment> | |||
</div> | |||
<div | |||
className="overview-sidebar page-sidebar-fixed" | |||
> | |||
<Connect(Meta) | |||
branchLike={ | |||
Object { | |||
"isMain": true, | |||
"name": "b", | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"analysisDate": "2016-01-01", | |||
"breadcrumbs": Array [], | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
"version": "0.0.1", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render another message when there are branches 2`] = ` | |||
<div | |||
className="page page-limited" | |||
> | |||
<div | |||
className="overview page-with-sidebar" | |||
> | |||
<div | |||
className="overview-main page-main sonarcloud" | |||
> | |||
<React.Fragment> | |||
<WarningMessage | |||
branchLike={ | |||
Object { | |||
"isMain": true, | |||
"name": "b", | |||
"type": "LONG", | |||
} | |||
} | |||
message="provisioning.no_analysis_on_main_branch.bad_configuration" | |||
/> | |||
</React.Fragment> | |||
</div> | |||
<div | |||
className="overview-sidebar page-sidebar-fixed" | |||
> | |||
<Connect(Meta) | |||
branchLike={ | |||
Object { | |||
"isMain": true, | |||
"name": "b", | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"analysisDate": "2016-01-01", | |||
"breadcrumbs": Array [], | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
"version": "0.0.1", | |||
} | |||
} | |||
onComponentChange={[MockFunction]} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render warning message 1`] = ` | |||
<div | |||
className="alert alert-warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="foo" | |||
id="foo" | |||
values={ | |||
Object { | |||
"branchName": "b", | |||
"branchType": <div | |||
className="outline-badge text-baseline" | |||
> | |||
branches.main_branch | |||
</div>, | |||
} | |||
} | |||
/> | |||
</div> | |||
`; |
@@ -1,71 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly with a "before" range 1`] = ` | |||
<LineChart | |||
data={ | |||
Array [ | |||
Object { | |||
"x": 0, | |||
"y": 29.6, | |||
}, | |||
Object { | |||
"x": 1, | |||
"y": 170.8, | |||
}, | |||
] | |||
} | |||
displayBackdrop={true} | |||
displayPoints={false} | |||
displayVerticalGrid={false} | |||
domain={ | |||
Array [ | |||
0, | |||
360, | |||
] | |||
} | |||
height={80} | |||
padding={ | |||
Array [ | |||
0, | |||
0, | |||
0, | |||
0, | |||
] | |||
} | |||
/> | |||
`; | |||
exports[`should render correctly with an "after" range 1`] = ` | |||
<LineChart | |||
data={ | |||
Array [ | |||
Object { | |||
"x": 0, | |||
"y": 360, | |||
}, | |||
Object { | |||
"x": 1, | |||
"y": 39, | |||
}, | |||
] | |||
} | |||
displayBackdrop={true} | |||
displayPoints={false} | |||
displayVerticalGrid={false} | |||
domain={ | |||
Array [ | |||
0, | |||
360, | |||
] | |||
} | |||
height={80} | |||
padding={ | |||
Array [ | |||
0, | |||
0, | |||
0, | |||
0, | |||
] | |||
} | |||
/> | |||
`; |
@@ -58,8 +58,8 @@ interface OwnProps { | |||
branchLike?: BranchLike; | |||
component: Component; | |||
history?: History; | |||
measures: MeasureEnhanced[]; | |||
metrics: { [key: string]: Metric }; | |||
measures?: MeasureEnhanced[]; | |||
metrics?: { [key: string]: Metric }; | |||
onComponentChange: (changes: {}) => void; | |||
} | |||
@@ -106,7 +106,7 @@ export class Meta extends React.PureComponent<Props> { | |||
render() { | |||
const { organizationsEnabled } = this.context; | |||
const { branchLike, component, metrics, organization } = this.props; | |||
const { branchLike, component, measures, metrics, organization } = this.props; | |||
const { qualifier, description, visibility } = component; | |||
const isProject = qualifier === 'TRK'; | |||
@@ -131,16 +131,20 @@ export class Meta extends React.PureComponent<Props> { | |||
{isProject && ( | |||
<MetaTags component={component} onComponentChange={this.props.onComponentChange} /> | |||
)} | |||
<MetaSize branchLike={branchLike} component={component} measures={this.props.measures} /> | |||
{measures && ( | |||
<MetaSize branchLike={branchLike} component={component} measures={measures} /> | |||
)} | |||
</div> | |||
<AnalysesList | |||
branchLike={branchLike} | |||
component={component} | |||
history={this.props.history} | |||
metrics={metrics} | |||
qualifier={component.qualifier} | |||
/> | |||
{metrics && ( | |||
<AnalysesList | |||
branchLike={branchLike} | |||
component={component} | |||
history={this.props.history} | |||
metrics={metrics} | |||
qualifier={component.qualifier} | |||
/> | |||
)} | |||
{this.renderQualityInfos()} | |||
@@ -152,7 +156,8 @@ export class Meta extends React.PureComponent<Props> { | |||
</div> | |||
{!isPrivate && | |||
(isProject || isApp) && ( | |||
(isProject || isApp) && | |||
metrics && ( | |||
<BadgesModal | |||
branchLike={branchLike} | |||
metrics={metrics} |
@@ -24,6 +24,7 @@ | |||
.overview-main { | |||
background-color: var(--barBackgroundColor); | |||
transition: transform 0.5s ease, opacity 0.5s ease; | |||
width: calc(100% - 300px); | |||
} | |||
.overview-sidebar { |
@@ -19,11 +19,13 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import * as PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import { InjectedRouter } from 'react-router'; | |||
import { Location } from 'history'; | |||
import Helmet from 'react-helmet'; | |||
import AutoProjectCreate from './AutoProjectCreate'; | |||
import ManualProjectCreate from './ManualProjectCreate'; | |||
import { serializeQuery, Query, parseQuery } from './utils'; | |||
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import { skipOnboarding } from '../../../store/users/actions'; | |||
@@ -32,10 +34,11 @@ 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 { | |||
location: Location; | |||
onFinishOnboarding: () => void; | |||
router: Pick<InjectedRouter, 'push' | 'replace'>; | |||
} | |||
interface StateProps { | |||
@@ -46,26 +49,16 @@ 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> { | |||
export class CreateProjectPage extends React.PureComponent<Props> { | |||
mounted = false; | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { activeTab: this.shouldDisplayTabs(props) ? Tabs.AUTO : Tabs.MANUAL }; | |||
if (!this.canAutoCreate(props)) { | |||
this.updateQuery({ manual: true }); | |||
} | |||
} | |||
componentDidMount() { | |||
@@ -85,13 +78,13 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> { | |||
handleProjectCreate = (projects: Pick<ProjectBase, 'key'>[], organization?: string) => { | |||
if (projects.length > 1 && organization) { | |||
this.context.router.push(getOrganizationUrl(organization) + '/projects'); | |||
this.props.router.push(getOrganizationUrl(organization) + '/projects'); | |||
} else if (projects.length === 1) { | |||
this.context.router.push(getProjectUrl(projects[0].key)); | |||
this.props.router.push(getProjectUrl(projects[0].key)); | |||
} | |||
}; | |||
shouldDisplayTabs = ({ currentUser } = this.props) => { | |||
canAutoCreate = ({ currentUser } = this.props) => { | |||
return ( | |||
isLoggedIn(currentUser) && | |||
['bitbucket', 'github'].includes(currentUser.externalProvider || '') | |||
@@ -100,12 +93,19 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> { | |||
showAuto = (event: React.MouseEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.setState({ activeTab: Tabs.AUTO }); | |||
this.updateQuery({ manual: false }); | |||
}; | |||
showManual = (event: React.MouseEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.setState({ activeTab: Tabs.MANUAL }); | |||
this.updateQuery({ manual: true }); | |||
}; | |||
updateQuery = (changes: Partial<Query>) => { | |||
this.props.router.replace({ | |||
pathname: this.props.location.pathname, | |||
query: serializeQuery({ ...parseQuery(this.props.location.query), ...changes }) | |||
}); | |||
}; | |||
render() { | |||
@@ -113,8 +113,7 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> { | |||
if (!isLoggedIn(currentUser)) { | |||
return null; | |||
} | |||
const { activeTab } = this.state; | |||
const displayManual = parseQuery(this.props.location.query).manual; | |||
const header = translate('onboarding.create_project.header'); | |||
return ( | |||
<> | |||
@@ -124,11 +123,11 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> { | |||
<h1 className="page-title">{header}</h1> | |||
</div> | |||
{this.shouldDisplayTabs() && ( | |||
{this.canAutoCreate() && ( | |||
<ul className="flex-tabs"> | |||
<li> | |||
<a | |||
className={classNames('js-auto', { selected: activeTab === Tabs.AUTO })} | |||
className={classNames('js-auto', { selected: !displayManual })} | |||
href="#" | |||
onClick={this.showAuto}> | |||
{translate('onboarding.create_project.select_repositories')} | |||
@@ -136,8 +135,8 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> { | |||
className={classNames( | |||
'rounded alert alert-small spacer-left display-inline-block', | |||
{ | |||
'alert-info': activeTab === Tabs.AUTO, | |||
'alert-muted': activeTab !== Tabs.AUTO | |||
'alert-info': !displayManual, | |||
'alert-muted': displayManual | |||
} | |||
)}> | |||
{translate('beta')} | |||
@@ -146,7 +145,7 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> { | |||
</li> | |||
<li> | |||
<a | |||
className={classNames('js-manual', { selected: activeTab === Tabs.MANUAL })} | |||
className={classNames('js-manual', { selected: displayManual })} | |||
href="#" | |||
onClick={this.showManual}> | |||
{translate('onboarding.create_project.create_manually')} | |||
@@ -155,13 +154,13 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> { | |||
</ul> | |||
)} | |||
{activeTab === Tabs.AUTO ? ( | |||
<AutoProjectCreate | |||
{displayManual || !this.canAutoCreate() ? ( | |||
<ManualProjectCreate | |||
currentUser={currentUser} | |||
onProjectCreate={this.handleProjectCreate} | |||
/> | |||
) : ( | |||
<ManualProjectCreate | |||
<AutoProjectCreate | |||
currentUser={currentUser} | |||
onProjectCreate={this.handleProjectCreate} | |||
/> | |||
@@ -181,5 +180,5 @@ const mapStateToProps = (state: any): StateProps => { | |||
const mapDispatchToProps: DispatchProps = { skipOnboarding }; | |||
export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)( | |||
CreateProjectOnboarding | |||
CreateProjectPage | |||
); |
@@ -63,7 +63,7 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> { | |||
projectName: '', | |||
projectKey: '', | |||
selectedOrganization: | |||
props.userOrganizations.length <= 1 ? props.userOrganizations[0].key : '', | |||
props.userOrganizations.length === 1 ? props.userOrganizations[0].key : '', | |||
submitting: false | |||
}; | |||
} |
@@ -19,7 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { CreateProjectOnboarding } from '../CreateProjectOnboarding'; | |||
import { Location } from 'history'; | |||
import { CreateProjectPage } from '../CreateProjectPage'; | |||
import { LoggedInUser } from '../../../../app/types'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
@@ -35,11 +36,16 @@ it('should render correctly', () => { | |||
}); | |||
it('should render with Manual creation only', () => { | |||
expect(getWrapper({ currentUser: { ...user, externalProvider: 'vsts' } })).toMatchSnapshot(); | |||
expect(getWrapper({ currentUser: { ...user, externalProvider: 'microsoft' } })).toMatchSnapshot(); | |||
}); | |||
it('should switch tabs', () => { | |||
const wrapper = getWrapper(); | |||
const replace = jest.fn(); | |||
const wrapper = getWrapper({ router: { replace } }); | |||
replace.mockImplementation(location => { | |||
wrapper.setProps({ location }).update(); | |||
}); | |||
click(wrapper.find('.js-manual')); | |||
expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy(); | |||
click(wrapper.find('.js-auto')); | |||
@@ -48,9 +54,11 @@ it('should switch tabs', () => { | |||
function getWrapper(props = {}) { | |||
return shallow( | |||
<CreateProjectOnboarding | |||
<CreateProjectPage | |||
currentUser={user} | |||
location={{ pathname: 'foo', query: { manual: 'false' } } as Location} | |||
onFinishOnboarding={jest.fn()} | |||
router={{ push: jest.fn(), replace: jest.fn() }} | |||
skipOnboarding={jest.fn()} | |||
{...props} | |||
/> |
@@ -85,7 +85,7 @@ exports[`should render with Manual creation only 1`] = ` | |||
<Connect(ManualProjectCreate) | |||
currentUser={ | |||
Object { | |||
"externalProvider": "vsts", | |||
"externalProvider": "microsoft", | |||
"isLoggedIn": true, | |||
"login": "foo", | |||
"name": "Foo", |
@@ -0,0 +1,42 @@ | |||
/* | |||
* 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 { memoize } from 'lodash'; | |||
import { | |||
cleanQuery, | |||
RawQuery, | |||
parseAsBoolean, | |||
serializeOptionalBoolean | |||
} from '../../../helpers/query'; | |||
export interface Query { | |||
manual: boolean; | |||
} | |||
export const parseQuery = memoize((urlQuery: RawQuery): Query => { | |||
return { | |||
manual: parseAsBoolean(urlQuery['manual'], false) | |||
}; | |||
}); | |||
export const serializeQuery = memoize((query: Query): RawQuery => | |||
cleanQuery({ | |||
manual: serializeOptionalBoolean(query.manual || undefined) | |||
}) | |||
); |
@@ -22,6 +22,8 @@ import DefaultPageSelectorContainer from './components/DefaultPageSelectorContai | |||
import FavoriteProjectsContainer from './components/FavoriteProjectsContainer'; | |||
import { PROJECTS_DEFAULT_FILTER, PROJECTS_ALL } from './utils'; | |||
import { save } from '../../helpers/storage'; | |||
import { isSonarCloud } from '../../helpers/system'; | |||
import { lazyLoad } from '../../components/lazyLoad'; | |||
const routes = [ | |||
{ indexRoute: { component: DefaultPageSelectorContainer } }, | |||
@@ -32,7 +34,11 @@ const routes = [ | |||
replace('/projects'); | |||
} | |||
}, | |||
{ path: 'favorite', component: FavoriteProjectsContainer } | |||
]; | |||
{ path: 'favorite', component: FavoriteProjectsContainer }, | |||
isSonarCloud() && { | |||
path: 'create', | |||
component: lazyLoad(() => import('./create/CreateProjectPage')) | |||
} | |||
].filter(Boolean); | |||
export default routes; |
@@ -0,0 +1,94 @@ | |||
/* | |||
* 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 AnalyzeTutorialSuggestion from './AnalyzeTutorialSuggestion'; | |||
import ProjectAnalysisStep from '../components/ProjectAnalysisStep'; | |||
import TokenStep from '../components/TokenStep'; | |||
import { Component, LoggedInUser } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import '../styles.css'; | |||
enum Steps { | |||
ANALYSIS, | |||
TOKEN | |||
} | |||
interface Props { | |||
component: Component; | |||
currentUser: LoggedInUser; | |||
} | |||
interface State { | |||
step: Steps; | |||
token?: string; | |||
} | |||
export default class AnalyzeTutorial extends React.PureComponent<Props, State> { | |||
state: State = { step: Steps.TOKEN }; | |||
handleTokenDone = (token: string) => { | |||
this.setState({ step: Steps.ANALYSIS, token }); | |||
}; | |||
handleTokenOpen = () => { | |||
this.setState({ step: Steps.TOKEN }); | |||
}; | |||
render() { | |||
const { component, currentUser } = this.props; | |||
const { step, token } = this.state; | |||
let stepNumber = 1; | |||
const almId = component.almId || currentUser.externalProvider; | |||
const showTutorial = almId !== 'microsoft'; | |||
return ( | |||
<> | |||
<div className="page-header big-spacer-bottom"> | |||
<h1 className="page-title">{translate('onboarding.project_analysis.header')}</h1> | |||
<p className="page-description">{translate('onboarding.project_analysis.description')}</p> | |||
</div> | |||
<AnalyzeTutorialSuggestion almId={almId} /> | |||
{showTutorial && ( | |||
<> | |||
<TokenStep | |||
currentUser={currentUser} | |||
finished={Boolean(this.state.token)} | |||
onContinue={this.handleTokenDone} | |||
onOpen={this.handleTokenOpen} | |||
open={step === Steps.TOKEN} | |||
stepNumber={stepNumber++} | |||
/> | |||
<ProjectAnalysisStep | |||
component={component} | |||
displayRowLayout={true} | |||
open={step === Steps.ANALYSIS} | |||
organization={component.organization} | |||
stepNumber={stepNumber++} | |||
token={token} | |||
/> | |||
</> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -0,0 +1,88 @@ | |||
/* | |||
* 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 { FormattedMessage } from 'react-intl'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
export default function AnalyzeTutorialSuggestion({ almId }: { almId?: string }) { | |||
if (almId && almId.startsWith('bitbucket')) { | |||
return ( | |||
<div className="alert alert-info big-spacer-bottom"> | |||
<p>{translate('onboarding.project_analysis.commands_for_analysis')}</p> | |||
<p>{translate('onboarding.project_analysis.suggestions.bitbucket')}</p> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.project_analysis.simply_link')} | |||
id={'onboarding.project_analysis.simply_link'} | |||
values={{ | |||
link: ( | |||
<a | |||
href={ | |||
getBaseUrl() + | |||
'/documentation/integrations/bitbucketcloud#analyzing-with-pipelines' | |||
} | |||
target="_blank"> | |||
{translate('onboarding.project_analysis.guide_to_integrate_piplines')} | |||
</a> | |||
) | |||
}} | |||
/> | |||
</div> | |||
); | |||
} else if (almId === 'github') { | |||
return ( | |||
<div className="alert alert-info big-spacer-bottom"> | |||
<p>{translate('onboarding.project_analysis.commands_for_analysis')} </p> | |||
<p>{translate('onboarding.project_analysis.suggestions.github')}</p> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.project_analysis.simply_link')} | |||
id={'onboarding.project_analysis.simply_link'} | |||
values={{ | |||
link: ( | |||
<a | |||
href="https://docs.travis-ci.com/user/sonarcloud/" | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
{translate('onboarding.project_analysis.guide_to_integrate_travis')} | |||
</a> | |||
) | |||
}} | |||
/> | |||
</div> | |||
); | |||
} else if (almId === 'microsoft') { | |||
return ( | |||
<p className="alert alert-info big-spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.project_analysis.simply_link')} | |||
id={'onboarding.project_analysis.simply_link'} | |||
values={{ | |||
link: ( | |||
<a href={getBaseUrl() + '/documentation/integrations/vsts'} target="_blank"> | |||
{translate('onboarding.project_analysis.guide_to_integrate_vsts')} | |||
</a> | |||
) | |||
}} | |||
/> | |||
</p> | |||
); | |||
} | |||
return null; | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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 AnalyzeTutorial from '../AnalyzeTutorial'; | |||
import { LoggedInUser } from '../../../../app/types'; | |||
const component = { | |||
key: 'foo', | |||
analysisDate: '2016-01-01', | |||
breadcrumbs: [], | |||
name: 'Foo', | |||
organization: 'org', | |||
qualifier: 'TRK', | |||
version: '0.0.1' | |||
}; | |||
const loggedInUser: LoggedInUser = { | |||
isLoggedIn: true, | |||
login: 'luke', | |||
name: 'Skywalker' | |||
}; | |||
it('renders correctly', () => { | |||
expect(getWrapper()).toMatchSnapshot(); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow(<AnalyzeTutorial component={component} currentUser={loggedInUser} {...props} />); | |||
} |
@@ -0,0 +1,38 @@ | |||
/* | |||
* 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 AnalyzeTutorialSuggestion from '../AnalyzeTutorialSuggestion'; | |||
it('should not render', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId={undefined} />).type()).toBeNull(); | |||
}); | |||
it('renders bitbucket suggestions correctly', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId="bitbucket" />)).toMatchSnapshot(); | |||
}); | |||
it('renders github suggestions correctly', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId="github" />)).toMatchSnapshot(); | |||
}); | |||
it('renders vsts suggestions correctly', () => { | |||
expect(shallow(<AnalyzeTutorialSuggestion almId="microsoft" />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,54 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders correctly 1`] = ` | |||
<React.Fragment> | |||
<div | |||
className="page-header big-spacer-bottom" | |||
> | |||
<h1 | |||
className="page-title" | |||
> | |||
onboarding.project_analysis.header | |||
</h1> | |||
<p | |||
className="page-description" | |||
> | |||
onboarding.project_analysis.description | |||
</p> | |||
</div> | |||
<AnalyzeTutorialSuggestion /> | |||
<React.Fragment> | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
"login": "luke", | |||
"name": "Skywalker", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
open={true} | |||
stepNumber={1} | |||
/> | |||
<ProjectAnalysisStep | |||
component={ | |||
Object { | |||
"analysisDate": "2016-01-01", | |||
"breadcrumbs": Array [], | |||
"key": "foo", | |||
"name": "Foo", | |||
"organization": "org", | |||
"qualifier": "TRK", | |||
"version": "0.0.1", | |||
} | |||
} | |||
displayRowLayout={true} | |||
open={false} | |||
organization="org" | |||
stepNumber={2} | |||
/> | |||
</React.Fragment> | |||
</React.Fragment> | |||
`; |
@@ -0,0 +1,78 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders bitbucket suggestions correctly 1`] = ` | |||
<div | |||
className="alert alert-info big-spacer-bottom" | |||
> | |||
<p> | |||
onboarding.project_analysis.commands_for_analysis | |||
</p> | |||
<p> | |||
onboarding.project_analysis.suggestions.bitbucket | |||
</p> | |||
<FormattedMessage | |||
defaultMessage="onboarding.project_analysis.simply_link" | |||
id="onboarding.project_analysis.simply_link" | |||
values={ | |||
Object { | |||
"link": <a | |||
href="/documentation/integrations/bitbucketcloud#analyzing-with-pipelines" | |||
target="_blank" | |||
> | |||
onboarding.project_analysis.guide_to_integrate_piplines | |||
</a>, | |||
} | |||
} | |||
/> | |||
</div> | |||
`; | |||
exports[`renders github suggestions correctly 1`] = ` | |||
<div | |||
className="alert alert-info big-spacer-bottom" | |||
> | |||
<p> | |||
onboarding.project_analysis.commands_for_analysis | |||
</p> | |||
<p> | |||
onboarding.project_analysis.suggestions.github | |||
</p> | |||
<FormattedMessage | |||
defaultMessage="onboarding.project_analysis.simply_link" | |||
id="onboarding.project_analysis.simply_link" | |||
values={ | |||
Object { | |||
"link": <a | |||
href="https://docs.travis-ci.com/user/sonarcloud/" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
onboarding.project_analysis.guide_to_integrate_travis | |||
</a>, | |||
} | |||
} | |||
/> | |||
</div> | |||
`; | |||
exports[`renders vsts suggestions correctly 1`] = ` | |||
<p | |||
className="alert alert-info big-spacer-bottom" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.project_analysis.simply_link" | |||
id="onboarding.project_analysis.simply_link" | |||
values={ | |||
Object { | |||
"link": <a | |||
href="/documentation/integrations/vsts" | |||
target="_blank" | |||
> | |||
onboarding.project_analysis.guide_to_integrate_vsts | |||
</a>, | |||
} | |||
} | |||
/> | |||
</p> | |||
`; |
@@ -23,39 +23,30 @@ import NewProjectForm from './NewProjectForm'; | |||
import RadioToggle from '../../../components/controls/RadioToggle'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
export interface Result { | |||
language?: string; | |||
javaBuild?: string; | |||
cFamilyCompiler?: string; | |||
os?: string; | |||
projectKey?: string; | |||
} | |||
import { Component } from '../../../app/types'; | |||
import { isLanguageConfigured, LanguageConfig } from '../utils'; | |||
interface Props { | |||
onDone: (result: Result) => void; | |||
component?: Component; | |||
config?: LanguageConfig; | |||
onDone: (config: LanguageConfig) => void; | |||
onReset: () => void; | |||
organization?: string; | |||
} | |||
type State = Result; | |||
export default class LanguageStep extends React.PureComponent<Props, State> { | |||
state: State = {}; | |||
isConfigured = () => { | |||
const { language, javaBuild, cFamilyCompiler, os, projectKey } = this.state; | |||
const isJavaConfigured = language === 'java' && javaBuild != null; | |||
const isDotNetConfigured = language === 'dotnet' && projectKey != null; | |||
const isCFamilyConfigured = | |||
language === 'c-family' && (cFamilyCompiler === 'msvc' || os != null) && projectKey != null; | |||
const isOtherConfigured = language === 'other' && projectKey != null; | |||
type State = LanguageConfig; | |||
return isJavaConfigured || isDotNetConfigured || isCFamilyConfigured || isOtherConfigured; | |||
}; | |||
export default class LanguageForm extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
...(this.props.config || {}), | |||
projectKey: props.component ? props.component.key : undefined | |||
}; | |||
} | |||
handleChange = () => { | |||
if (this.isConfigured()) { | |||
if (isLanguageConfigured(this.state)) { | |||
this.props.onDone(this.state); | |||
} else { | |||
this.props.onReset(); | |||
@@ -131,29 +122,36 @@ export default class LanguageStep extends React.PureComponent<Props, State> { | |||
</div> | |||
); | |||
renderProjectKey = () => ( | |||
<NewProjectForm | |||
onDelete={this.handleProjectKeyDelete} | |||
onDone={this.handleProjectKeyDone} | |||
organization={this.props.organization} | |||
projectKey={this.state.projectKey} | |||
/> | |||
); | |||
renderProjectKey = () => { | |||
const { cFamilyCompiler, language, os } = this.state; | |||
const needProjectKey = | |||
language === 'dotnet' || | |||
(language === 'c-family' && | |||
(cFamilyCompiler === 'msvc' || (cFamilyCompiler === 'clang-gcc' && os !== undefined))) || | |||
(language === 'other' && os !== undefined); | |||
render() { | |||
const shouldAskProjectKey = | |||
this.state.language === 'dotnet' || | |||
(this.state.language === 'c-family' && | |||
(this.state.cFamilyCompiler === 'msvc' || | |||
(this.state.cFamilyCompiler === 'clang-gcc' && this.state.os != null))) || | |||
(this.state.language === 'other' && this.state.os !== undefined); | |||
if (!needProjectKey || this.props.component) { | |||
return null; | |||
} | |||
return ( | |||
<NewProjectForm | |||
onDelete={this.handleProjectKeyDelete} | |||
onDone={this.handleProjectKeyDone} | |||
organization={this.props.organization} | |||
projectKey={this.state.projectKey} | |||
/> | |||
); | |||
}; | |||
render() { | |||
const { cFamilyCompiler, language } = this.state; | |||
const languages = isSonarCloud() | |||
? ['java', 'dotnet', 'c-family', 'other'] | |||
: ['java', 'dotnet', 'other']; | |||
return ( | |||
<div> | |||
<> | |||
<div> | |||
<h4 className="spacer-bottom">{translate('onboarding.language')}</h4> | |||
<RadioToggle | |||
@@ -163,16 +161,15 @@ export default class LanguageStep extends React.PureComponent<Props, State> { | |||
label: translate('onboarding.language', language), | |||
value: language | |||
}))} | |||
value={this.state.language} | |||
value={language} | |||
/> | |||
</div> | |||
{this.state.language === 'java' && this.renderJavaBuild()} | |||
{this.state.language === 'c-family' && this.renderCFamilyCompiler()} | |||
{((this.state.language === 'c-family' && this.state.cFamilyCompiler === 'clang-gcc') || | |||
this.state.language === 'other') && | |||
{language === 'java' && this.renderJavaBuild()} | |||
{language === 'c-family' && this.renderCFamilyCompiler()} | |||
{((language === 'c-family' && cFamilyCompiler === 'clang-gcc') || language === 'other') && | |||
this.renderOS()} | |||
{shouldAskProjectKey && this.renderProjectKey()} | |||
</div> | |||
{this.renderProjectKey()} | |||
</> | |||
); | |||
} | |||
} |
@@ -0,0 +1,120 @@ | |||
/* | |||
* 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 Step from './Step'; | |||
import LanguageForm from './LanguageForm'; | |||
import AnalysisCommand from './commands/AnalysisCommand'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Component } from '../../../app/types'; | |||
import { LanguageConfig } from '../utils'; | |||
interface Props { | |||
component?: Component; | |||
displayRowLayout?: boolean; | |||
onFinish?: (projectKey?: string) => void; | |||
onReset?: () => void; | |||
open: boolean; | |||
organization?: string; | |||
stepNumber: number; | |||
token?: string; | |||
} | |||
interface State { | |||
config?: LanguageConfig; | |||
} | |||
export default class ProjectAnalysisStep extends React.PureComponent<Props, State> { | |||
state: State = {}; | |||
getProjectKey = ({ config } = this.state, { component } = this.props) => { | |||
return (component && component.key) || (config && config.projectKey); | |||
}; | |||
handleLanguageSelect = (config: LanguageConfig) => { | |||
this.setState({ config }); | |||
if (this.props.onFinish) { | |||
const projectKey = config.language !== 'java' ? this.getProjectKey({ config }) : undefined; | |||
this.props.onFinish(projectKey); | |||
} | |||
}; | |||
handleLanguageReset = () => { | |||
this.setState({ config: undefined }); | |||
if (this.props.onReset) { | |||
this.props.onReset(); | |||
} | |||
}; | |||
renderForm = () => { | |||
const languageComponent = ( | |||
<LanguageForm | |||
component={this.props.component} | |||
onDone={this.handleLanguageSelect} | |||
onReset={this.handleLanguageReset} | |||
organization={this.props.organization} | |||
/> | |||
); | |||
const analysisComponent = this.state.config && ( | |||
<AnalysisCommand | |||
component={this.props.component} | |||
languageConfig={this.state.config} | |||
organization={this.props.organization} | |||
small={true} | |||
token={this.props.token} | |||
/> | |||
); | |||
if (this.props.displayRowLayout) { | |||
return ( | |||
<div className="boxed-group-inner"> | |||
<div className="display-flex-column"> | |||
{languageComponent} | |||
{analysisComponent && <div className="huge-spacer-top">{analysisComponent}</div>} | |||
</div> | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className="boxed-group-inner"> | |||
<div className="flex-columns"> | |||
<div className="flex-column flex-column-half bordered-right">{languageComponent}</div> | |||
<div className="flex-column flex-column-half">{analysisComponent}</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderResult = () => null; | |||
render() { | |||
return ( | |||
<Step | |||
finished={false} | |||
onOpen={() => {}} | |||
open={this.props.open} | |||
renderForm={this.renderForm} | |||
renderResult={this.renderResult} | |||
stepNumber={this.props.stepNumber} | |||
stepTitle={translate('onboarding.analysis.header')} | |||
/> | |||
); | |||
} | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import LanguageStep from '../LanguageStep'; | |||
import LanguageForm from '../LanguageForm'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | |||
@@ -30,7 +30,7 @@ beforeEach(() => { | |||
it('selects java', () => { | |||
const onDone = jest.fn(); | |||
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />); | |||
const wrapper = shallow(<LanguageForm onDone={onDone} onReset={jest.fn()} />); | |||
(wrapper.find('RadioToggle').prop('onCheck') as Function)('java'); | |||
wrapper.update(); | |||
@@ -55,7 +55,7 @@ it('selects java', () => { | |||
it('selects c#', () => { | |||
const onDone = jest.fn(); | |||
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />); | |||
const wrapper = shallow(<LanguageForm onDone={onDone} onReset={jest.fn()} />); | |||
(wrapper.find('RadioToggle').prop('onCheck') as Function)('dotnet'); | |||
wrapper.update(); | |||
@@ -68,7 +68,7 @@ it('selects c#', () => { | |||
it('selects c-family', () => { | |||
(isSonarCloud as jest.Mock<any>).mockImplementation(() => true); | |||
const onDone = jest.fn(); | |||
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />); | |||
const wrapper = shallow(<LanguageForm onDone={onDone} onReset={jest.fn()} />); | |||
(wrapper.find('RadioToggle').prop('onCheck') as Function)('c-family'); | |||
wrapper.update(); | |||
@@ -113,7 +113,7 @@ it('selects c-family', () => { | |||
it('selects other', () => { | |||
const onDone = jest.fn(); | |||
const wrapper = shallow(<LanguageStep onDone={onDone} onReset={jest.fn()} />); | |||
const wrapper = shallow(<LanguageForm onDone={onDone} onReset={jest.fn()} />); | |||
(wrapper.find('RadioToggle').prop('onCheck') as Function)('other'); | |||
wrapper.update(); |
@@ -1,7 +1,7 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`selects c# 1`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -35,11 +35,11 @@ exports[`selects c# 1`] = ` | |||
onDelete={[Function]} | |||
onDone={[Function]} | |||
/> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects c-family 1`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -100,11 +100,11 @@ exports[`selects c-family 1`] = ` | |||
value={null} | |||
/> | |||
</div> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects c-family 2`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -169,11 +169,11 @@ exports[`selects c-family 2`] = ` | |||
onDelete={[Function]} | |||
onDone={[Function]} | |||
/> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects c-family 3`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -265,11 +265,11 @@ exports[`selects c-family 3`] = ` | |||
value={null} | |||
/> | |||
</div> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects c-family 4`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -366,11 +366,11 @@ exports[`selects c-family 4`] = ` | |||
onDone={[Function]} | |||
projectKey="project-foo" | |||
/> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects java 1`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -427,11 +427,11 @@ exports[`selects java 1`] = ` | |||
value={null} | |||
/> | |||
</div> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects java 2`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -488,11 +488,11 @@ exports[`selects java 2`] = ` | |||
value="maven" | |||
/> | |||
</div> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects java 3`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -549,11 +549,11 @@ exports[`selects java 3`] = ` | |||
value="gradle" | |||
/> | |||
</div> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects other 1`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -614,11 +614,11 @@ exports[`selects other 1`] = ` | |||
value={null} | |||
/> | |||
</div> | |||
</div> | |||
</React.Fragment> | |||
`; | |||
exports[`selects other 2`] = ` | |||
<div> | |||
<React.Fragment> | |||
<div> | |||
<h4 | |||
className="spacer-bottom" | |||
@@ -683,5 +683,5 @@ exports[`selects other 2`] = ` | |||
onDelete={[Function]} | |||
onDone={[Function]} | |||
/> | |||
</div> | |||
</React.Fragment> | |||
`; |
@@ -0,0 +1,146 @@ | |||
/* | |||
* 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 JavaMaven from './JavaMaven'; | |||
import JavaGradle from './JavaGradle'; | |||
import DotNet from './DotNet'; | |||
import Msvc from './Msvc'; | |||
import ClangGCC from './ClangGCC'; | |||
import Other from './Other'; | |||
import { getHostUrl } from '../../../../helpers/urls'; | |||
import { Component } from '../../../../app/types'; | |||
import { LanguageConfig } from '../../utils'; | |||
interface Props { | |||
component?: Component; | |||
organization?: string; | |||
languageConfig: LanguageConfig; | |||
small?: boolean; | |||
token?: string; | |||
} | |||
export default class AnalysisCommand extends React.PureComponent<Props> { | |||
getProjectKey = ({ component, languageConfig } = this.props) => { | |||
return (component && component.key) || languageConfig.projectKey; | |||
}; | |||
renderCommandForMaven = () => { | |||
const { token } = this.props; | |||
if (!token) { | |||
return null; | |||
} | |||
return <JavaMaven host={getHostUrl()} organization={this.props.organization} token={token} />; | |||
}; | |||
renderCommandForGradle = () => { | |||
const { token } = this.props; | |||
if (!token) { | |||
return null; | |||
} | |||
return <JavaGradle host={getHostUrl()} organization={this.props.organization} token={token} />; | |||
}; | |||
renderCommandForDotNet = () => { | |||
const { small, token } = this.props; | |||
const projectKey = this.getProjectKey(); | |||
if (!projectKey || !token) { | |||
return null; | |||
} | |||
return ( | |||
<DotNet | |||
host={getHostUrl()} | |||
organization={this.props.organization} | |||
projectKey={projectKey} | |||
small={small} | |||
token={token} | |||
/> | |||
); | |||
}; | |||
renderCommandForMSVC = () => { | |||
const { small, token } = this.props; | |||
const projectKey = this.getProjectKey(); | |||
if (!projectKey || !token) { | |||
return null; | |||
} | |||
return ( | |||
<Msvc | |||
host={getHostUrl()} | |||
organization={this.props.organization} | |||
projectKey={projectKey} | |||
small={small} | |||
token={token} | |||
/> | |||
); | |||
}; | |||
renderCommandForClangGCC = () => { | |||
const { languageConfig, small, token } = this.props; | |||
const projectKey = this.getProjectKey(); | |||
if (!languageConfig || !projectKey || !languageConfig.os || !token) { | |||
return null; | |||
} | |||
return ( | |||
<ClangGCC | |||
host={getHostUrl()} | |||
organization={this.props.organization} | |||
os={languageConfig.os} | |||
projectKey={projectKey} | |||
small={small} | |||
token={token} | |||
/> | |||
); | |||
}; | |||
renderCommandForOther = () => { | |||
const { languageConfig, token } = this.props; | |||
const projectKey = this.getProjectKey(); | |||
if (!languageConfig || !projectKey || !languageConfig.os || !token) { | |||
return null; | |||
} | |||
return ( | |||
<Other | |||
host={getHostUrl()} | |||
organization={this.props.organization} | |||
os={languageConfig.os} | |||
projectKey={projectKey} | |||
token={token} | |||
/> | |||
); | |||
}; | |||
render() { | |||
const { languageConfig } = this.props; | |||
if (languageConfig.language === 'java') { | |||
return languageConfig.javaBuild === 'maven' | |||
? this.renderCommandForMaven() | |||
: this.renderCommandForGradle(); | |||
} else if (languageConfig.language === 'dotnet') { | |||
return this.renderCommandForDotNet(); | |||
} else if (languageConfig.language === 'c-family') { | |||
return languageConfig.cFamilyCompiler === 'msvc' | |||
? this.renderCommandForMSVC() | |||
: this.renderCommandForClangGCC(); | |||
} else { | |||
return this.renderCommandForOther(); | |||
} | |||
} | |||
} |
@@ -29,6 +29,7 @@ interface Props { | |||
os: string; | |||
organization?: string; | |||
projectKey: string; | |||
small?: boolean; | |||
token: string; | |||
} | |||
@@ -67,7 +68,7 @@ export default function ClangGCC(props: Props) { | |||
/> | |||
)} | |||
</InstanceMessage> | |||
<CodeSnippet isOneLine={true} snippet={command1} /> | |||
<CodeSnippet isOneLine={props.small} snippet={command1} /> | |||
<CodeSnippet isOneLine={props.os === 'win'} snippet={command2} /> | |||
<p | |||
className="big-spacer-top markdown" |
@@ -27,6 +27,7 @@ interface Props { | |||
host: string; | |||
organization?: string; | |||
projectKey: string; | |||
small?: boolean; | |||
token: string; | |||
} | |||
@@ -59,8 +60,8 @@ export default function DotNet(props: Props) { | |||
)} | |||
</InstanceMessage> | |||
<CodeSnippet isOneLine={true} snippet={command1} /> | |||
<CodeSnippet isOneLine={true} snippet={command2} /> | |||
<CodeSnippet isOneLine={true} snippet={command3} /> | |||
<CodeSnippet isOneLine={false} snippet={command2} /> | |||
<CodeSnippet isOneLine={props.small} snippet={command3} /> | |||
<p | |||
className="big-spacer-top markdown" | |||
dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.docs') }} |
@@ -61,7 +61,9 @@ export default function JavaGradle(props: Props) { | |||
<p | |||
className="big-spacer-top markdown" | |||
dangerouslySetInnerHTML={{ | |||
__html: translate('onboarding.analysis.browse_url_after_analysis') | |||
__html: props.projectKey | |||
? translate('onboarding.analysis.auto_refresh_after_analysis') | |||
: translate('onboarding.analysis.browse_url_after_analysis') | |||
}} | |||
/> | |||
</div> |
@@ -50,7 +50,9 @@ export default function JavaMaven(props: Props) { | |||
<p | |||
className="big-spacer-top markdown" | |||
dangerouslySetInnerHTML={{ | |||
__html: translate('onboarding.analysis.browse_url_after_analysis') | |||
__html: props.projectKey | |||
? translate('onboarding.analysis.auto_refresh_after_analysis') | |||
: translate('onboarding.analysis.browse_url_after_analysis') | |||
}} | |||
/> | |||
</div> |
@@ -28,6 +28,7 @@ interface Props { | |||
host: string; | |||
organization?: string; | |||
projectKey: string; | |||
small?: boolean; | |||
token: string; | |||
} | |||
@@ -62,8 +63,8 @@ export default function Msvc(props: Props) { | |||
)} | |||
</InstanceMessage> | |||
<CodeSnippet isOneLine={true} snippet={command1} /> | |||
<CodeSnippet isOneLine={true} snippet={command2} /> | |||
<CodeSnippet isOneLine={true} snippet={command3} /> | |||
<CodeSnippet isOneLine={props.small} snippet={command2} /> | |||
<CodeSnippet isOneLine={props.small} snippet={command3} /> | |||
<p | |||
className="big-spacer-top markdown" | |||
dangerouslySetInnerHTML={{ __html: translate('onboarding.analysis.msbuild.docs') }} |
@@ -0,0 +1,71 @@ | |||
/* | |||
* 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 AnalysisCommand from '../AnalysisCommand'; | |||
jest.mock('../../../../../helpers/system', () => ({ | |||
isSonarCloud: jest.fn().mockReturnValue(true) | |||
})); | |||
it('display java command', () => { | |||
expect( | |||
getWrapper({ languageConfig: { language: 'java', javaBuild: 'gradle' } }) | |||
).toMatchSnapshot(); | |||
expect( | |||
getWrapper({ languageConfig: { language: 'java', javaBuild: 'maven' } }) | |||
).toMatchSnapshot(); | |||
}); | |||
it('display c# command', () => { | |||
expect( | |||
getWrapper({ languageConfig: { language: 'dotnet', projectKey: 'project-foo' } }) | |||
).toMatchSnapshot(); | |||
}); | |||
it('display c-family command', () => { | |||
expect( | |||
getWrapper({ | |||
languageConfig: { language: 'c-family', cFamilyCompiler: 'msvc', projectKey: 'project-foo' } | |||
}) | |||
).toMatchSnapshot(); | |||
expect( | |||
getWrapper({ | |||
languageConfig: { | |||
language: 'c-family', | |||
cFamilyCompiler: 'clang-gcc', | |||
os: 'linux', | |||
projectKey: 'project-foo' | |||
} | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('display others command', () => { | |||
expect( | |||
getWrapper({ | |||
languageConfig: { language: 'other', os: 'window', projectKey: 'project-foo' } | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
function getWrapper(props = {}) { | |||
return shallow(<AnalysisCommand languageConfig={{}} token="myToken" {...props} />); | |||
} |
@@ -38,6 +38,7 @@ it('renders correctly', () => { | |||
organization="organization" | |||
os="linux" | |||
projectKey="projectKey" | |||
small={true} | |||
token="token" | |||
/> | |||
) |
@@ -26,7 +26,13 @@ it('renders correctly', () => { | |||
expect(shallow(<DotNet host="host" projectKey="projectKey" token="token" />)).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<DotNet host="host" organization="organization" projectKey="projectKey" token="token" /> | |||
<DotNet | |||
host="host" | |||
organization="organization" | |||
projectKey="projectKey" | |||
small={true} | |||
token="token" | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -25,6 +25,14 @@ import Msvc from '../Msvc'; | |||
it('renders correctly', () => { | |||
expect(shallow(<Msvc host="host" projectKey="projectKey" token="token" />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<Msvc host="host" organization="organization" projectKey="projectKey" token="token" />) | |||
shallow( | |||
<Msvc | |||
host="host" | |||
organization="organization" | |||
projectKey="projectKey" | |||
small={true} | |||
token="token" | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,49 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`display c# command 1`] = ` | |||
<DotNet | |||
host="null" | |||
projectKey="project-foo" | |||
token="myToken" | |||
/> | |||
`; | |||
exports[`display c-family command 1`] = ` | |||
<Msvc | |||
host="null" | |||
projectKey="project-foo" | |||
token="myToken" | |||
/> | |||
`; | |||
exports[`display c-family command 2`] = ` | |||
<ClangGCC | |||
host="null" | |||
os="linux" | |||
projectKey="project-foo" | |||
token="myToken" | |||
/> | |||
`; | |||
exports[`display java command 1`] = ` | |||
<JavaGradle | |||
host="null" | |||
token="myToken" | |||
/> | |||
`; | |||
exports[`display java command 2`] = ` | |||
<JavaMaven | |||
host="null" | |||
token="myToken" | |||
/> | |||
`; | |||
exports[`display others command 1`] = ` | |||
<Other | |||
host="null" | |||
os="window" | |||
projectKey="project-foo" | |||
token="myToken" | |||
/> | |||
`; |
@@ -18,7 +18,6 @@ exports[`renders correctly 1`] = ` | |||
message="onboarding.analysis.sq_scanner.execute.text" | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="build-wrapper-win-x86-64.exe --out-dir bw-output make clean all" | |||
/> | |||
<CodeSnippet | |||
@@ -64,7 +63,6 @@ exports[`renders correctly 2`] = ` | |||
message="onboarding.analysis.sq_scanner.execute.text" | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="build-wrapper-linux-x86-64 --out-dir bw-output make clean all" | |||
/> | |||
<CodeSnippet |
@@ -24,11 +24,10 @@ exports[`renders correctly 1`] = ` | |||
} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
isOneLine={false} | |||
snippet="MsBuild.exe /t:Rebuild" | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet={ | |||
Array [ | |||
"SonarScanner.MSBuild.exe end", | |||
@@ -71,7 +70,7 @@ exports[`renders correctly 2`] = ` | |||
} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
isOneLine={false} | |||
snippet="MsBuild.exe /t:Rebuild" | |||
/> | |||
<CodeSnippet |
@@ -91,7 +91,7 @@ exports[`renders correctly 2`] = ` | |||
className="big-spacer-top markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "onboarding.analysis.browse_url_after_analysis", | |||
"__html": "onboarding.analysis.auto_refresh_after_analysis", | |||
} | |||
} | |||
/> |
@@ -79,7 +79,7 @@ exports[`renders correctly 2`] = ` | |||
className="big-spacer-top markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "onboarding.analysis.browse_url_after_analysis", | |||
"__html": "onboarding.analysis.auto_refresh_after_analysis", | |||
} | |||
} | |||
/> |
@@ -29,11 +29,9 @@ exports[`renders correctly 1`] = ` | |||
} | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="build-wrapper-win-x86-64.exe --out-dir bw-output MsBuild.exe /t:Rebuild" | |||
/> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet={ | |||
Array [ | |||
"SonarQube.Scanner.MSBuild.exe end", |
@@ -19,16 +19,16 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication'; | |||
import Modal from '../../components/controls/Modal'; | |||
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'; | |||
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; | |||
import Modal from '../../../components/controls/Modal'; | |||
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 { | |||
onClose: () => void; | |||
@@ -43,7 +43,7 @@ interface StateProps { | |||
type Props = OwnProps & StateProps; | |||
export class Onboarding extends React.PureComponent<Props> { | |||
export class OnboardingModal extends React.PureComponent<Props> { | |||
componentDidMount() { | |||
if (!isLoggedIn(this.props.currentUser)) { | |||
handleRequiredAuthentication(); | |||
@@ -96,4 +96,4 @@ export class Onboarding extends React.PureComponent<Props> { | |||
const mapStateToProps = (state: any): StateProps => ({ currentUser: getCurrentUser(state) }); | |||
export default connect<StateProps, {}, OwnProps>(mapStateToProps)(Onboarding); | |||
export default connect<StateProps, {}, OwnProps>(mapStateToProps)(OnboardingModal); |
@@ -0,0 +1,99 @@ | |||
/* | |||
* 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 PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import OnboardingModal from './OnboardingModal'; | |||
import { skipOnboarding } from '../../../api/users'; | |||
import { skipOnboarding as skipOnboardingAction } from '../../../store/users/actions'; | |||
import CreateOrganizationForm from '../../account/organizations/CreateOrganizationForm'; | |||
import TeamOnboardingModal from '../teamOnboarding/TeamOnboardingModal'; | |||
import { Organization } from '../../../app/types'; | |||
interface DispatchProps { | |||
skipOnboardingAction: () => void; | |||
} | |||
enum ModalKey { | |||
onboarding, | |||
organizationOnboarding, | |||
teamOnboarding | |||
} | |||
interface State { | |||
modal?: ModalKey; | |||
} | |||
export class OnboardingPage extends React.PureComponent<DispatchProps, State> { | |||
static contextTypes = { | |||
openProjectOnboarding: PropTypes.func.isRequired, | |||
router: PropTypes.object.isRequired | |||
}; | |||
state: State = { modal: ModalKey.onboarding }; | |||
closeOnboarding = () => { | |||
skipOnboarding(); | |||
this.props.skipOnboardingAction(); | |||
this.context.router.replace('/'); | |||
}; | |||
closeOrganizationOnboarding = ({ key }: Pick<Organization, 'key'>) => { | |||
this.closeOnboarding(); | |||
this.context.router.push(`/organizations/${key}`); | |||
}; | |||
openOrganizationOnboarding = () => { | |||
this.setState({ modal: ModalKey.organizationOnboarding }); | |||
}; | |||
openTeamOnboarding = () => { | |||
this.setState({ modal: ModalKey.teamOnboarding }); | |||
}; | |||
render() { | |||
const { modal } = this.state; | |||
return ( | |||
<> | |||
{modal === ModalKey.onboarding && ( | |||
<OnboardingModal | |||
onClose={this.closeOnboarding} | |||
onOpenOrganizationOnboarding={this.openOrganizationOnboarding} | |||
onOpenProjectOnboarding={this.context.openProjectOnboarding} | |||
onOpenTeamOnboarding={this.openTeamOnboarding} | |||
/> | |||
)} | |||
{modal === ModalKey.organizationOnboarding && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeOnboarding} | |||
onCreate={this.closeOrganizationOnboarding} | |||
/> | |||
)} | |||
{modal === ModalKey.teamOnboarding && ( | |||
<TeamOnboardingModal onFinish={this.closeOnboarding} /> | |||
)} | |||
</> | |||
); | |||
} | |||
} | |||
const mapDispatchToProps: DispatchProps = { skipOnboardingAction }; | |||
export default connect<{}, DispatchProps>(null, mapDispatchToProps)(OnboardingPage); |
@@ -19,13 +19,13 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { Onboarding } from '../Onboarding'; | |||
import { click } from '../../../helpers/testUtils'; | |||
import { OnboardingModal } from '../OnboardingModal'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('renders correctly', () => { | |||
expect( | |||
shallow( | |||
<Onboarding | |||
<OnboardingModal | |||
currentUser={{ isLoggedIn: true }} | |||
onClose={jest.fn()} | |||
onOpenOrganizationOnboarding={jest.fn()} | |||
@@ -43,7 +43,7 @@ it('should correctly open the different tutorials', () => { | |||
const onOpenTeamOnboarding = jest.fn(); | |||
const push = jest.fn(); | |||
const wrapper = shallow( | |||
<Onboarding | |||
<OnboardingModal | |||
currentUser={{ isLoggedIn: true }} | |||
onClose={onClose} | |||
onOpenOrganizationOnboarding={onOpenOrganizationOnboarding} |