From 19fb2bab0cfcd5067bbde14f5b686b5f0360c27e Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 13 Jun 2017 16:36:06 +0200 Subject: [PATCH] SONAR-9358 Display a first analysis spinner in the onboarding tutorial --- server/sonar-web/src/main/js/api/ce.js | 4 +- .../components/nav/component/ComponentNav.js | 2 +- .../apps/tutorials/onboarding/AnalysisStep.js | 4 +- .../apps/tutorials/onboarding/Onboarding.js | 58 ++++++--- .../tutorials/onboarding/ProjectWatcher.js | 121 ++++++++++++++++++ .../__tests__/ProjectWatcher-test.js | 59 +++++++++ .../__snapshots__/ProjectWatcher-test.js.snap | 42 ++++++ .../resources/org/sonar/l10n/core.properties | 5 + 8 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/ProjectWatcher.js create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/ProjectWatcher-test.js create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/ProjectWatcher-test.js.snap 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 && - } + (this.state.projectKey + ? + : )} ); } 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 ( +
+ + {translate('onboarding.project_watcher.finished')} +
+ ); + } + + if (inQueue || status === STATUSES.PENDING || status === STATUSES.IN_PROGRESS) { + return ( +
+ + {translate('onboarding.project_watcher.in_progress')} +
+ ); + } + + if (status != null) { + return ( +
+ + {translate('onboarding.project_watcher.failed')} +
+ ); + } + + return ( +
+ {translate('onboarding.project_watcher.not_started')} +
+ ); + } +} 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( + + ); + 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(); + expect(onFinish).not.toBeCalled(); + jest.runTimersToTime(5000); +}); + +it('timeouts', () => { + const onTimeout = jest.fn(); + mount(); + 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`] = ` +
+ onboarding.project_watcher.not_started +
+`; + +exports[`renders 2`] = ` +
+ + onboarding.project_watcher.in_progress +
+`; + +exports[`renders 3`] = ` +
+ + onboarding.project_watcher.finished +
+`; + +exports[`renders 4`] = ` +
+ + onboarding.project_watcher.in_progress +
+`; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ef5ccb3eb8e..7c830e984a6 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3024,3 +3024,8 @@ onboarding.analysis.sq_scanner.text.mac=And add the bin 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 official documentation of the SonarQube Scanner 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. -- 2.39.5