@@ -38,7 +38,7 @@ export const cancelTask = (id: string): Promise<*> => | |||
export const cancelAllTasks = (): Promise<*> => post('/api/ce/cancel_all'); | |||
export const getTasksForComponent = (componentId: string): Promise<*> => | |||
getJSON('/api/ce/component', { componentId }); | |||
export const getTasksForComponent = (componentKey: string): Promise<*> => | |||
getJSON('/api/ce/component', { componentKey }); | |||
export const getTypes = (): Promise<*> => getJSON('/api/ce/task_types').then(r => r.taskTypes); |
@@ -40,7 +40,7 @@ export default class ComponentNav extends React.PureComponent { | |||
} | |||
loadStatus = () => { | |||
getTasksForComponent(this.props.component.id).then(r => { | |||
getTasksForComponent(this.props.component.key).then(r => { | |||
if (this.mounted) { | |||
this.setState({ | |||
isPending: r.queue.some(task => task.status === STATUSES.PENDING), |
@@ -31,7 +31,7 @@ import Other from './commands/Other'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
onFinish: () => void, | |||
onFinish: (projectKey?: string) => void, | |||
onReset: () => void, | |||
open: boolean, | |||
organization?: string, | |||
@@ -50,7 +50,7 @@ export default class AnalysisStep extends React.PureComponent { | |||
handleLanguageSelect = (result?: Result) => { | |||
this.setState({ result }); | |||
this.props.onFinish(); | |||
this.props.onFinish(result && result.projectKey); | |||
}; | |||
handleLanguageReset = () => { |
@@ -22,8 +22,10 @@ import React from 'react'; | |||
import TokenStep from './TokenStep'; | |||
import OrganizationStep from './OrganizationStep'; | |||
import AnalysisStep from './AnalysisStep'; | |||
import ProjectWatcher from './ProjectWatcher'; | |||
import { skipOnboarding } from '../../../api/users'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; | |||
import './styles.css'; | |||
@@ -37,6 +39,7 @@ type Props = { | |||
type State = { | |||
finished: boolean, | |||
organization?: string, | |||
projectKey?: string, | |||
skipping: boolean, | |||
step: string, | |||
token?: string | |||
@@ -47,6 +50,10 @@ export default class Onboarding extends React.PureComponent { | |||
props: Props; | |||
state: State; | |||
static contextTypes = { | |||
router: React.PropTypes.object | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
@@ -67,21 +74,16 @@ export default class Onboarding extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
handleTokenDone = (token: string) => { | |||
this.setState({ step: 'analysis', token }); | |||
}; | |||
handleOrganizationDone = (organization: string) => { | |||
this.setState({ organization, step: 'token' }); | |||
}; | |||
handleSkipClick = (event: Event) => { | |||
event.preventDefault(); | |||
finishOnboarding = () => { | |||
this.setState({ skipping: true }); | |||
skipOnboarding().then( | |||
() => { | |||
if (this.mounted) { | |||
this.props.onSkip(); | |||
if (this.state.projectKey) { | |||
this.context.router.push(getProjectUrl(this.state.projectKey)); | |||
} | |||
} | |||
}, | |||
() => { | |||
@@ -92,7 +94,25 @@ export default class Onboarding extends React.PureComponent { | |||
); | |||
}; | |||
handleFinish = () => this.setState({ finished: true }); | |||
handleTimeout = () => { | |||
// unset `projectKey` to display a generic "Finish this tutorial" button | |||
this.setState({ projectKey: undefined }); | |||
}; | |||
handleTokenDone = (token: string) => { | |||
this.setState({ step: 'analysis', token }); | |||
}; | |||
handleOrganizationDone = (organization: string) => { | |||
this.setState({ organization, step: 'token' }); | |||
}; | |||
handleSkipClick = (event: Event) => { | |||
event.preventDefault(); | |||
this.finishOnboarding(); | |||
}; | |||
handleFinish = (projectKey?: string) => this.setState({ finished: true, projectKey }); | |||
handleReset = () => this.setState({ finished: false }); | |||
@@ -150,11 +170,17 @@ export default class Onboarding extends React.PureComponent { | |||
{this.state.finished && | |||
!this.state.skipping && | |||
<footer className="text-right"> | |||
<a className="button" href="#" onClick={this.handleSkipClick}> | |||
{translate('tutorials.finish')} | |||
</a> | |||
</footer>} | |||
(this.state.projectKey | |||
? <ProjectWatcher | |||
onFinish={this.finishOnboarding} | |||
onTimeout={this.handleTimeout} | |||
projectKey={this.state.projectKey} | |||
/> | |||
: <footer className="text-right"> | |||
<a className="button" href="#" onClick={this.handleSkipClick}> | |||
{translate('tutorials.finish')} | |||
</a> | |||
</footer>)} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,121 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { getTasksForComponent } from '../../../api/ce'; | |||
import { STATUSES } from '../../../apps/background-tasks/constants'; | |||
import { translate } from '../../../helpers/l10n'; | |||
const INTERVAL = 5000; | |||
const TIMEOUT = 10 * 60 * 1000; // 10 min | |||
type Props = { | |||
onFinish: () => void, | |||
onTimeout: () => void, | |||
projectKey: string | |||
}; | |||
type State = { | |||
inQueue: boolean, | |||
status: ?string | |||
}; | |||
export default class ProjectWatcher extends React.PureComponent { | |||
interval: number; | |||
mounted: boolean; | |||
props: Props; | |||
timeout: number; | |||
state: State = { | |||
inQueue: false, | |||
status: null | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.watch(); | |||
this.timeout = setTimeout(this.props.onTimeout, TIMEOUT); | |||
} | |||
componentWillUnmount() { | |||
clearInterval(this.interval); | |||
clearInterval(this.timeout); | |||
this.mounted = false; | |||
} | |||
watch = () => (this.interval = setTimeout(this.checkProject, INTERVAL)); | |||
checkProject = () => { | |||
const { projectKey } = this.props; | |||
getTasksForComponent(projectKey).then(response => { | |||
if (response.queue.length > 0) { | |||
this.setState({ inQueue: true }); | |||
} | |||
if (response.current != null) { | |||
const { status } = response.current; | |||
this.setState({ status }); | |||
if (status === STATUSES.SUCCESS) { | |||
this.props.onFinish(); | |||
} else if (status === STATUSES.PENDING || status === STATUSES.IN_PROGRESS) { | |||
this.watch(); | |||
} | |||
} else { | |||
this.watch(); | |||
} | |||
}); | |||
}; | |||
render() { | |||
const { inQueue, status } = this.state; | |||
if (status === STATUSES.SUCCESS) { | |||
return ( | |||
<div className="big-spacer-top note text-center"> | |||
<i className="icon-check spacer-right" /> | |||
{translate('onboarding.project_watcher.finished')} | |||
</div> | |||
); | |||
} | |||
if (inQueue || status === STATUSES.PENDING || status === STATUSES.IN_PROGRESS) { | |||
return ( | |||
<div className="big-spacer-top note text-center"> | |||
<i className="spinner spacer-right" /> | |||
{translate('onboarding.project_watcher.in_progress')} | |||
</div> | |||
); | |||
} | |||
if (status != null) { | |||
return ( | |||
<div className="big-spacer-top note text-center"> | |||
<i className="icon-alert-danger spacer-right" /> | |||
{translate('onboarding.project_watcher.failed')} | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className="big-spacer-top note text-center"> | |||
{translate('onboarding.project_watcher.not_started')} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,59 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { shallow, mount } from 'enzyme'; | |||
import ProjectWatcher from '../ProjectWatcher'; | |||
jest.mock('../../../../api/ce', () => ({ | |||
getTasksForComponent: () => Promise.resolve({ current: { status: 'SUCCESS' }, queue: [] }) | |||
})); | |||
jest.useFakeTimers(); | |||
it('renders', () => { | |||
const wrapper = shallow( | |||
<ProjectWatcher onFinish={jest.fn()} onTimeout={jest.fn()} projectKey="foo" /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ inQueue: true }); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ status: 'SUCCESS' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ status: 'FAILED' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('finishes', done => { | |||
// checking `expect(onFinish).toBeCalled();` is not working, because it's called asynchronously | |||
// instead let's finish the test as soon as `onFinish` callback is called | |||
const onFinish = jest.fn(done); | |||
mount(<ProjectWatcher onFinish={onFinish} onTimeout={jest.fn()} projectKey="foo" />); | |||
expect(onFinish).not.toBeCalled(); | |||
jest.runTimersToTime(5000); | |||
}); | |||
it('timeouts', () => { | |||
const onTimeout = jest.fn(); | |||
mount(<ProjectWatcher onFinish={jest.fn()} onTimeout={onTimeout} projectKey="foo" />); | |||
expect(onTimeout).not.toBeCalled(); | |||
jest.runTimersToTime(10 * 60 * 1000); | |||
expect(onTimeout).toBeCalled(); | |||
}); |
@@ -0,0 +1,42 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div | |||
className="big-spacer-top note text-center" | |||
> | |||
onboarding.project_watcher.not_started | |||
</div> | |||
`; | |||
exports[`renders 2`] = ` | |||
<div | |||
className="big-spacer-top note text-center" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
onboarding.project_watcher.in_progress | |||
</div> | |||
`; | |||
exports[`renders 3`] = ` | |||
<div | |||
className="big-spacer-top note text-center" | |||
> | |||
<i | |||
className="icon-check spacer-right" | |||
/> | |||
onboarding.project_watcher.finished | |||
</div> | |||
`; | |||
exports[`renders 4`] = ` | |||
<div | |||
className="big-spacer-top note text-center" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
onboarding.project_watcher.in_progress | |||
</div> | |||
`; |
@@ -3024,3 +3024,8 @@ onboarding.analysis.sq_scanner.text.mac=And add the <code>bin</code> directory t | |||
onboarding.analysis.sq_scanner.execute=Execute the SonarQube Scanner from your computer | |||
onboarding.analysis.sq_scanner.execute.text=Running a SonarQube analysis is straighforward. You just need to execute the following commands in your project's folder. | |||
onboarding.analysis.sq_scanner.docs=Please visit the <a href="http://redirect.sonarsource.com/doc/install-configure-scanner.html" target="_blank">official documentation of the SonarQube Scanner</a> for more details. | |||
onboarding.project_watcher.not_started=Once your project is analyzed, this page will refresh automatically. | |||
onboarding.project_watcher.in_progress=Analysis is in progress, please wait... | |||
onboarding.project_watcher.finished=Analysis is finished, redirecting... | |||
onboarding.project_watcher.failed=Something went wrong, please check the analysis logs. |