]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9358 Display a first analysis spinner in the onboarding tutorial
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 13 Jun 2017 14:36:06 +0000 (16:36 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 20 Jun 2017 11:10:53 +0000 (04:10 -0700)
server/sonar-web/src/main/js/api/ce.js
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/AnalysisStep.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 4f73f4cc948c544495cd2b03a4ce6060dd9bd445..bc4eb2283e25246d44ee8395bc57a19152966eac 100644 (file)
@@ -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);
index 4893ce58f0e4eef4197fa7f64982504a8376b324..23b08126fa76d30f5381c3b0b2996a52e1399dae 100644 (file)
@@ -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),
index 573551790a41b7be50b93a4cf832a3ae1a830a2c..83b7319a0132f3dc97f343ec8c291d89c2d85bcb 100644 (file)
@@ -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 = () => {
index e2617224cc09dd1d6de6bf62ff7c82a3af9fca3b..06a5de99f38f21654d5468d81b61b06d73ffa388 100644 (file)
@@ -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 (file)
index 0000000..920547f
--- /dev/null
@@ -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 (file)
index 0000000..5a2cc71
--- /dev/null
@@ -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 (file)
index 0000000..91088ad
--- /dev/null
@@ -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>
+`;
index ef5ccb3eb8e5f335a9f82daad2a5ebcf4cdbe0b1..7c830e984a6870086dac9671f2333a6720210a7f 100644 (file)
@@ -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.