Kaynağa Gözat

SONAR-9358 Display a first analysis spinner in the onboarding tutorial

tags/6.5-M2
Stas Vilchik 7 yıl önce
ebeveyn
işleme
19fb2bab0c

+ 2
- 2
server/sonar-web/src/main/js/api/ce.js Dosyayı Görüntüle

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

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js Dosyayı Görüntüle

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

+ 2
- 2
server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js Dosyayı Görüntüle

@@ -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 = () => {

+ 42
- 16
server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js Dosyayı Görüntüle

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

+ 121
- 0
server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js Dosyayı Görüntüle

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

+ 59
- 0
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js Dosyayı Görüntüle

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

+ 42
- 0
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap Dosyayı Görüntüle

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

+ 5
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Dosyayı Görüntüle

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

Loading…
İptal
Kaydet