aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-06-13 16:36:06 +0200
committerStas Vilchik <stas.vilchik@sonarsource.com>2017-06-20 04:10:53 -0700
commit19fb2bab0cfcd5067bbde14f5b686b5f0360c27e (patch)
tree5c60d79f20c7c3a6f248b5a25fe7fd67899d2505 /server/sonar-web/src/main/js
parent71531284395e2d0f674a2942235b78974dd9a35f (diff)
downloadsonarqube-19fb2bab0cfcd5067bbde14f5b686b5f0360c27e.tar.gz
sonarqube-19fb2bab0cfcd5067bbde14f5b686b5f0360c27e.zip
SONAR-9358 Display a first analysis spinner in the onboarding tutorial
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/api/ce.js4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js2
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js4
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js58
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js121
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js59
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap42
7 files changed, 269 insertions, 21 deletions
diff --git a/server/sonar-web/src/main/js/api/ce.js b/server/sonar-web/src/main/js/api/ce.js
index 4f73f4cc948..bc4eb2283e2 100644
--- a/server/sonar-web/src/main/js/api/ce.js
+++ b/server/sonar-web/src/main/js/api/ce.js
@@ -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);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
index 4893ce58f0e..23b08126fa7 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
@@ -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),
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js
index 573551790a4..83b7319a013 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js
@@ -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 = () => {
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
index e2617224cc0..06a5de99f38 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js
new file mode 100644
index 00000000000..920547fcab0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js
new file mode 100644
index 00000000000..5a2cc714d03
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap
new file mode 100644
index 00000000000..91088ada48f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap
@@ -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>
+`;