]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite maintenance app in react (#3055)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 14 Feb 2018 09:51:22 +0000 (10:51 +0100)
committerGitHub <noreply@github.com>
Wed, 14 Feb 2018 09:51:22 +0000 (10:51 +0100)
43 files changed:
server/sonar-web/src/main/js/api/system.ts
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Form-test.tsx
server/sonar-web/src/main/js/apps/custom-metrics/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/maintenance/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/maintenance/components/MaintenanceAppContainer.tsx
server/sonar-web/src/main/js/apps/maintenance/components/SetupAppContainer.tsx
server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/maintenance/init.js [deleted file]
server/sonar-web/src/main/js/apps/maintenance/main-view.js [deleted file]
server/sonar-web/src/main/js/apps/maintenance/routes.tsx
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-failed.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-not-supported.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-required.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-running.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-succeeded.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-no-migration.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-down.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-migration.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-offline.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-starting.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-up.hbs [deleted file]
server/sonar-web/src/main/js/apps/maintenance/templates/maintenance-main.hbs [deleted file]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/LicenseEditionSet-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Subscription-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissions-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileRules-test.tsx
server/sonar-web/src/main/js/apps/sessions/components/__tests__/EmailAlreadyExists-test.tsx
server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeNotif-test.tsx
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewOrganizationForm-test.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/NewProjectForm-test.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/SimpleModal-test.tsx
server/sonar-web/src/main/js/components/lazyLoad.tsx
server/sonar-web/src/main/js/helpers/testUtils.ts

index 794000c0389ae23bd87ac77b3d03ddcfd6290733..597669a2dabe28e56ec21328ad3ab84cd5616405 100644 (file)
@@ -76,7 +76,7 @@ export function getSystemInfo(): Promise<SysInfo> {
   return getJSON('/api/system/info').catch(throwGlobalError);
 }
 
-export function getSystemStatus(): Promise<any> {
+export function getSystemStatus(): Promise<{ id: string; version: string; status: string }> {
   return getJSON('/api/system/status');
 }
 
@@ -87,11 +87,19 @@ export function getSystemUpgrades(): Promise<{
   return getJSON('/api/system/upgrades');
 }
 
-export function getMigrationStatus(): Promise<any> {
+export function getMigrationStatus(): Promise<{
+  message?: string;
+  startedAt?: string;
+  state: string;
+}> {
   return getJSON('/api/system/db_migration_status');
 }
 
-export function migrateDatabase() {
+export function migrateDatabase(): Promise<{
+  message?: string;
+  startedAt?: string;
+  state: string;
+}> {
   return postJSON('/api/system/migrate_db');
 }
 
index a264af89975fb0343099fef1672c0f634a4b6613..3b2019fe566082df0ad070c980db06103d876f7c 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import App from '../App';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/measures', () => ({
   getCustomMeasures: () =>
@@ -55,8 +56,7 @@ it('should work', async () => {
   const wrapper = shallow(<App component={{ key: 'foo' }} />);
   expect(wrapper).toMatchSnapshot();
 
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 
   // create
@@ -65,8 +65,7 @@ it('should work', async () => {
     metricKey: 'metricKey',
     value: 'value'
   });
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.state().measures).toMatchSnapshot();
   expect(wrapper.state().paging.total).toBe(2);
 
@@ -76,15 +75,13 @@ it('should work', async () => {
     id: '2',
     value: 'other'
   });
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.state().measures).toMatchSnapshot();
   expect(wrapper.state().paging.total).toBe(2);
 
   // delete
   wrapper.find('List').prop<Function>('onDelete')('2');
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.state().measures).toMatchSnapshot();
   expect(wrapper.state().paging.total).toBe(1);
 });
index 3af7a3c000a6c01f7a40850fa721d38332f94cce..e6252db478f1c3e6f15fdc65b6ae458bad7a9256 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import Form from '../Form';
-import { change, submit, click } from '../../../../helpers/testUtils';
+import { change, submit, click, waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/metrics', () => ({
   getAllMetrics: () =>
@@ -44,8 +44,7 @@ it('should render form', async () => {
   );
   expect(wrapper.dive()).toMatchSnapshot();
 
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   const form = wrapper.dive();
   expect(form).toMatchSnapshot();
 
index d4b9e0772d5b26e07bb03211ec719ff90f22d2ce..76ca8e0510f19537b03855d7e1004d6338f14a45 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import App from '../App';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/metrics', () => ({
   getMetricDomains: () => Promise.resolve(['Coverage', 'Issues']),
@@ -42,8 +43,7 @@ it('should work', async () => {
   (wrapper.instance() as App).mounted = true;
   expect(wrapper).toMatchSnapshot();
 
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 
   // create
@@ -53,8 +53,7 @@ it('should work', async () => {
     name: 'Bar',
     type: 'INT'
   });
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.state().metrics).toMatchSnapshot();
   expect(wrapper.state().paging.total).toBe(2);
 
@@ -66,15 +65,13 @@ it('should work', async () => {
     name: 'Bar',
     type: 'STRING'
   });
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.state().metrics).toMatchSnapshot();
   expect(wrapper.state().paging.total).toBe(2);
 
   // delete
   wrapper.find('List').prop<Function>('onDelete')('bar');
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.state().metrics).toMatchSnapshot();
   expect(wrapper.state().paging.total).toBe(1);
 });
diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx b/server/sonar-web/src/main/js/apps/maintenance/components/App.tsx
new file mode 100644 (file)
index 0000000..3bc5e06
--- /dev/null
@@ -0,0 +1,285 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { getMigrationStatus, getSystemStatus, migrateDatabase } from '../../../api/system';
+import DateFromNow from '../../../components/intl/DateFromNow';
+import TimeFormatter from '../../../components/intl/TimeFormatter';
+import { translate } from '../../../helpers/l10n';
+import { getBaseUrl } from '../../../helpers/urls';
+import '../styles.css';
+
+interface Props {
+  // eslint-disable-next-line camelcase
+  location: { query: { return_to?: string } };
+  setup: boolean;
+}
+
+interface State {
+  message?: string;
+  startedAt?: string;
+  state?: string;
+  status?: string;
+  wasStarting?: boolean;
+}
+
+export default class App extends React.PureComponent<Props, State> {
+  interval?: number;
+  mounted = false;
+  state: State = {};
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchStatus();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    if (this.interval) {
+      window.clearInterval(this.interval);
+    }
+  }
+
+  fetchStatus = () => {
+    const request = this.props.setup ? this.fetchMigrationState() : this.fetchSystemStatus();
+    request.catch(() => {
+      if (this.mounted) {
+        this.setState({
+          message: undefined,
+          startedAt: undefined,
+          state: undefined,
+          status: 'OFFLINE'
+        });
+      }
+    });
+  };
+
+  fetchSystemStatus = () => {
+    return getSystemStatus().then(({ status }) => {
+      if (this.mounted) {
+        this.setState({ status });
+
+        if (status === 'STARTING') {
+          this.setState({ wasStarting: true });
+          this.scheduleRefresh();
+        } else if (status === 'UP') {
+          if (this.state.wasStarting) {
+            this.loadPreviousPage();
+          }
+        } else {
+          this.scheduleRefresh();
+        }
+      }
+    });
+  };
+
+  fetchMigrationState = () => {
+    return getMigrationStatus().then(({ message, startedAt, state }) => {
+      if (this.mounted) {
+        this.setState({ message, startedAt, state });
+        if (state === 'MIGRATION_SUCCEEDED') {
+          this.loadPreviousPage();
+        } else if (state !== 'NO_MIGRATION') {
+          this.scheduleRefresh();
+        }
+      }
+    });
+  };
+
+  scheduleRefresh = () => {
+    this.interval = window.setTimeout(this.fetchStatus, 5000);
+  };
+
+  loadPreviousPage = () => {
+    setInterval(() => {
+      window.location.href = this.props.location.query['return_to'] || getBaseUrl() + '/';
+    }, 2500);
+  };
+
+  handleMigrateClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    migrateDatabase().then(
+      ({ message, startedAt, state }) => {
+        if (this.mounted) {
+          this.setState({ message, startedAt, state });
+        }
+      },
+      () => {}
+    );
+  };
+
+  render() {
+    const { state, status } = this.state;
+
+    return (
+      <div className="page-wrapper-simple" id="bd">
+        <div
+          className={classNames('page-simple', { 'panel-warning': state === 'MIGRATION_REQUIRED' })}
+          id="nonav">
+          {status === 'OFFLINE' && (
+            <>
+              <h1 className="maintenance-title text-danger">
+                {translate('maintenance.sonarqube_is_offline')}
+              </h1>
+              <p className="maintenance-text">
+                {translate('maintenance.sonarqube_is_offline.text')}
+              </p>
+              <p className="maintenance-text text-center">
+                <a href={getBaseUrl() + '/'}>{translate('maintenance.try_again')}</a>
+              </p>
+            </>
+          )}
+
+          {status === 'UP' && (
+            <>
+              <h1 className="maintenance-title">{translate('maintenance.sonarqube_is_up')}</h1>
+              <p className="maintenance-text text-center">
+                {translate('maintenance.all_systems_opetational')}
+              </p>
+              <p className="maintenance-text text-center">
+                <a href={getBaseUrl() + '/'}>{translate('layout.home')}</a>
+              </p>
+            </>
+          )}
+
+          {status === 'STARTING' && (
+            <>
+              <h1 className="maintenance-title">
+                {translate('maintenance.sonarqube_is_starting')}
+              </h1>
+              <p className="maintenance-spinner">
+                <i className="spinner" />
+              </p>
+            </>
+          )}
+
+          {status === 'DOWN' && (
+            <>
+              <h1 className="maintenance-title text-danger">
+                {translate('maintenance.sonarqube_is_down')}
+              </h1>
+              <p className="maintenance-text">{translate('maintenance.sonarqube_is_down.text')}</p>
+              <p className="maintenance-text text-center">
+                <a href={getBaseUrl() + '/'}>{translate('maintenance.try_again')}</a>
+              </p>
+            </>
+          )}
+
+          {(status === 'DB_MIGRATION_NEEDED' || status === 'DB_MIGRATION_RUNNING') && (
+            <>
+              <h1 className="maintenance-title">
+                {translate('maintenance.sonarqube_is_under_maintenance')}
+              </h1>
+              <p
+                className="maintenance-text"
+                dangerouslySetInnerHTML={{
+                  __html: translate('maintenance.sonarqube_is_under_maintenance.1')
+                }}
+              />
+              <p
+                className="maintenance-text"
+                dangerouslySetInnerHTML={{
+                  __html: translate('maintenance.sonarqube_is_under_maintenance.2')
+                }}
+              />
+            </>
+          )}
+
+          {state === 'NO_MIGRATION' && (
+            <>
+              <h1 className="maintenance-title">
+                {translate('maintenance.database_is_up_to_date')}
+              </h1>
+              <p className="maintenance-text text-center">
+                <a href={getBaseUrl() + '/'}>{translate('layout.home')}</a>
+              </p>
+            </>
+          )}
+
+          {state === 'MIGRATION_REQUIRED' && (
+            <>
+              <h1 className="maintenance-title">{translate('maintenance.upgrade_database')}</h1>
+              <p className="maintenance-text">{translate('maintenance.upgrade_database.1')}</p>
+              <p className="maintenance-text">{translate('maintenance.upgrade_database.2')}</p>
+              <p className="maintenance-text">{translate('maintenance.upgrade_database.3')}</p>
+              <div className="maintenance-spinner">
+                <button id="start-migration" onClick={this.handleMigrateClick} type="button">
+                  {translate('maintenance.upgrade')}
+                </button>
+              </div>
+            </>
+          )}
+
+          {state === 'NOT_SUPPORTED' && (
+            <>
+              <h1 className="maintenance-title text-danger">
+                {translate('maintenance.migration_not_supported')}
+              </h1>
+              <p>{translate('maintenance.migration_not_supported.text')}</p>
+            </>
+          )}
+
+          {state === 'MIGRATION_RUNNING' && (
+            <>
+              <h1 className="maintenance-title">{translate('maintenance.database_migration')}</h1>
+              {this.state.message && (
+                <p className="maintenance-text text-center">{this.state.message}</p>
+              )}
+              {this.state.startedAt && (
+                <p className="maintenance-text text-center">
+                  {translate('background_tasks.table.started')}{' '}
+                  <DateFromNow date={this.state.startedAt} />
+                  <br />
+                  <small className="text-muted">
+                    <TimeFormatter date={this.state.startedAt} long={true} />
+                  </small>
+                </p>
+              )}
+              <p className="maintenance-spinner">
+                <i className="spinner" />
+              </p>
+            </>
+          )}
+
+          {state === 'MIGRATION_SUCCEEDED' && (
+            <>
+              <h1 className="maintenance-title text-success">
+                {translate('maintenance.database_is_up_to_date')}
+              </h1>
+              <p className="maintenance-text text-center">
+                <a href={getBaseUrl() + '/'}>{translate('layout.home')}</a>
+              </p>
+            </>
+          )}
+
+          {state === 'MIGRATION_FAILED' && (
+            <>
+              <h1 className="maintenance-title text-danger">
+                {translate('maintenance.upgrade_failed')}
+              </h1>
+              <p className="maintenance-text">{translate('maintenance.upgrade_failed.text')}</p>
+            </>
+          )}
+        </div>
+      </div>
+    );
+  }
+}
index 38be383b8300fba423c979f46398b987dde98ef4..1a84ccdcd6df875bfc037e2288afe2f8b6f08eb7 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import init from '../init';
-import '../styles.css';
+import App from './App';
 
 interface Props {
+  // eslint-disable-next-line camelcase
   location: { query: { return_to: string } };
 }
 
-export default class MaintenanceAppContainer extends React.PureComponent<Props> {
-  componentDidMount() {
-    init(this.refs.container, false, this.props.location.query['return_to']);
-  }
-
-  render() {
-    return <div ref="container" />;
-  }
+export default function MaintenanceAppContainer(props: Props) {
+  return <App setup={false} {...props} />;
 }
index 46d68176189683b5f6dd8e7b58b8e64d71597751..4cd1e8fe5b51e868c334a6dbe831a279cf485f66 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import init from '../init';
-import '../styles.css';
+import App from './App';
 
 interface Props {
+  // eslint-disable-next-line camelcase
   location: { query: { return_to: string } };
 }
 
-export default class SetupAppContainer extends React.PureComponent<Props> {
-  componentDidMount() {
-    init(this.refs.container, true, this.props.location.query['return_to']);
-  }
-
-  render() {
-    return <div ref="container" />;
-  }
+export default function MaintenanceAppContainer(props: Props) {
+  return <App setup={true} {...props} />;
 }
diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..8d4511d
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+/* eslint-disable import/order */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import App from '../App';
+import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/system', () => ({
+  getMigrationStatus: jest.fn(),
+  getSystemStatus: jest.fn(),
+  migrateDatabase: jest.fn()
+}));
+
+jest.useFakeTimers();
+
+const getMigrationStatus = require('../../../../api/system').getMigrationStatus as jest.Mock;
+const getSystemStatus = require('../../../../api/system').getSystemStatus as jest.Mock;
+const migrateDatabase = require('../../../../api/system').migrateDatabase as jest.Mock;
+
+const location = { query: {} };
+
+beforeEach(() => {
+  getMigrationStatus.mockClear();
+  getSystemStatus.mockClear();
+  migrateDatabase.mockClear();
+});
+
+afterEach(() => {
+  jest.clearAllTimers();
+});
+
+describe('Maintenance Page', () => {
+  ['UP', 'DOWN', 'STARTING', 'DB_MIGRATION_NEEDED', 'DB_MIGRATION_RUNNING'].forEach(status => {
+    it(`should render ${status} status`, async () => {
+      getSystemStatus.mockImplementationOnce(() => Promise.resolve({ status }));
+      await checkApp(false);
+    });
+  });
+
+  it('should render OFFLINE status', async () => {
+    getSystemStatus.mockImplementationOnce(() => Promise.reject(undefined));
+    await checkApp(false);
+  });
+
+  it('should poll status', async () => {
+    getSystemStatus.mockImplementationOnce(() =>
+      Promise.resolve({ status: 'DB_MIGRATION_RUNNING' })
+    );
+    const wrapper = shallow(<App location={location} setup={false} />);
+    await waitAndUpdate(wrapper);
+    expect(getSystemStatus).toBeCalled();
+
+    getSystemStatus.mockClear();
+    getSystemStatus.mockImplementationOnce(() =>
+      Promise.resolve({ status: 'DB_MIGRATION_RUNNING' })
+    );
+    jest.runOnlyPendingTimers();
+    await waitAndUpdate(wrapper);
+    expect(getSystemStatus).toBeCalled();
+
+    getSystemStatus.mockClear();
+    getSystemStatus.mockImplementationOnce(() =>
+      Promise.resolve({ status: 'DB_MIGRATION_RUNNING' })
+    );
+    jest.runOnlyPendingTimers();
+    await waitAndUpdate(wrapper);
+    expect(getSystemStatus).toBeCalled();
+  });
+
+  it('should open previous page', async () => {
+    getSystemStatus.mockImplementationOnce(() => Promise.resolve({ status: 'STARTING' }));
+    const wrapper = shallow(<App location={location} setup={false} />);
+    const loadPreviousPage = jest.fn();
+    (wrapper.instance() as App).loadPreviousPage = loadPreviousPage;
+    await waitAndUpdate(wrapper);
+
+    getSystemStatus.mockImplementationOnce(() => Promise.resolve({ status: 'UP' }));
+    jest.runOnlyPendingTimers();
+    await waitAndUpdate(wrapper);
+    expect(loadPreviousPage).toBeCalled();
+  });
+});
+
+describe('Setup Page', () => {
+  ['NO_MIGRATION', 'NOT_SUPPORTED', 'MIGRATION_SUCCEEDED', 'MIGRATION_FAILED'].forEach(state => {
+    it(`should render ${state} state`, async () => {
+      getMigrationStatus.mockImplementationOnce(() =>
+        Promise.resolve({ message: 'message', startedAt: '2017-01-02T00:00:00.000Z', state })
+      );
+      await checkApp(true);
+    });
+  });
+
+  it('should start migration', async () => {
+    getMigrationStatus.mockImplementationOnce(() =>
+      Promise.resolve({ state: 'MIGRATION_REQUIRED' })
+    );
+    migrateDatabase.mockImplementationOnce(() =>
+      Promise.resolve({ startedAt: '2017-01-02T00:00:00.000Z', state: 'MIGRATION_RUNNING' })
+    );
+    const wrapper = shallow(<App location={location} setup={true} />);
+    await waitAndUpdate(wrapper);
+    expect(wrapper).toMatchSnapshot();
+
+    click(wrapper.find('button'));
+    expect(migrateDatabase).toBeCalled();
+    await waitAndUpdate(wrapper);
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+async function checkApp(setup: boolean) {
+  const wrapper = shallow(<App location={location} setup={setup} />);
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+}
diff --git a/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/maintenance/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644 (file)
index 0000000..784a8eb
--- /dev/null
@@ -0,0 +1,399 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Maintenance Page should render DB_MIGRATION_NEEDED status 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title"
+      >
+        maintenance.sonarqube_is_under_maintenance
+      </h1>
+      <p
+        className="maintenance-text"
+        dangerouslySetInnerHTML={
+          Object {
+            "__html": "maintenance.sonarqube_is_under_maintenance.1",
+          }
+        }
+      />
+      <p
+        className="maintenance-text"
+        dangerouslySetInnerHTML={
+          Object {
+            "__html": "maintenance.sonarqube_is_under_maintenance.2",
+          }
+        }
+      />
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Maintenance Page should render DB_MIGRATION_RUNNING status 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title"
+      >
+        maintenance.sonarqube_is_under_maintenance
+      </h1>
+      <p
+        className="maintenance-text"
+        dangerouslySetInnerHTML={
+          Object {
+            "__html": "maintenance.sonarqube_is_under_maintenance.1",
+          }
+        }
+      />
+      <p
+        className="maintenance-text"
+        dangerouslySetInnerHTML={
+          Object {
+            "__html": "maintenance.sonarqube_is_under_maintenance.2",
+          }
+        }
+      />
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Maintenance Page should render DOWN status 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title text-danger"
+      >
+        maintenance.sonarqube_is_down
+      </h1>
+      <p
+        className="maintenance-text"
+      >
+        maintenance.sonarqube_is_down.text
+      </p>
+      <p
+        className="maintenance-text text-center"
+      >
+        <a
+          href="/"
+        >
+          maintenance.try_again
+        </a>
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Maintenance Page should render OFFLINE status 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title text-danger"
+      >
+        maintenance.sonarqube_is_offline
+      </h1>
+      <p
+        className="maintenance-text"
+      >
+        maintenance.sonarqube_is_offline.text
+      </p>
+      <p
+        className="maintenance-text text-center"
+      >
+        <a
+          href="/"
+        >
+          maintenance.try_again
+        </a>
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Maintenance Page should render STARTING status 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title"
+      >
+        maintenance.sonarqube_is_starting
+      </h1>
+      <p
+        className="maintenance-spinner"
+      >
+        <i
+          className="spinner"
+        />
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Maintenance Page should render UP status 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title"
+      >
+        maintenance.sonarqube_is_up
+      </h1>
+      <p
+        className="maintenance-text text-center"
+      >
+        maintenance.all_systems_opetational
+      </p>
+      <p
+        className="maintenance-text text-center"
+      >
+        <a
+          href="/"
+        >
+          layout.home
+        </a>
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Setup Page should render MIGRATION_FAILED state 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title text-danger"
+      >
+        maintenance.upgrade_failed
+      </h1>
+      <p
+        className="maintenance-text"
+      >
+        maintenance.upgrade_failed.text
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Setup Page should render MIGRATION_SUCCEEDED state 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title text-success"
+      >
+        maintenance.database_is_up_to_date
+      </h1>
+      <p
+        className="maintenance-text text-center"
+      >
+        <a
+          href="/"
+        >
+          layout.home
+        </a>
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Setup Page should render NO_MIGRATION state 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title"
+      >
+        maintenance.database_is_up_to_date
+      </h1>
+      <p
+        className="maintenance-text text-center"
+      >
+        <a
+          href="/"
+        >
+          layout.home
+        </a>
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Setup Page should render NOT_SUPPORTED state 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title text-danger"
+      >
+        maintenance.migration_not_supported
+      </h1>
+      <p>
+        maintenance.migration_not_supported.text
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Setup Page should start migration 1`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple panel-warning"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title"
+      >
+        maintenance.upgrade_database
+      </h1>
+      <p
+        className="maintenance-text"
+      >
+        maintenance.upgrade_database.1
+      </p>
+      <p
+        className="maintenance-text"
+      >
+        maintenance.upgrade_database.2
+      </p>
+      <p
+        className="maintenance-text"
+      >
+        maintenance.upgrade_database.3
+      </p>
+      <div
+        className="maintenance-spinner"
+      >
+        <button
+          id="start-migration"
+          onClick={[Function]}
+          type="button"
+        >
+          maintenance.upgrade
+        </button>
+      </div>
+    </React.Fragment>
+  </div>
+</div>
+`;
+
+exports[`Setup Page should start migration 2`] = `
+<div
+  className="page-wrapper-simple"
+  id="bd"
+>
+  <div
+    className="page-simple"
+    id="nonav"
+  >
+    <React.Fragment>
+      <h1
+        className="maintenance-title"
+      >
+        maintenance.database_migration
+      </h1>
+      <p
+        className="maintenance-text text-center"
+      >
+        background_tasks.table.started
+         
+        <DateFromNow
+          date="2017-01-02T00:00:00.000Z"
+        />
+        <br />
+        <small
+          className="text-muted"
+        >
+          <TimeFormatter
+            date="2017-01-02T00:00:00.000Z"
+            long={true}
+          />
+        </small>
+      </p>
+      <p
+        className="maintenance-spinner"
+      >
+        <i
+          className="spinner"
+        />
+      </p>
+    </React.Fragment>
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/maintenance/init.js b/server/sonar-web/src/main/js/apps/maintenance/init.js
deleted file mode 100644 (file)
index 15c84c2..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import MainView from './main-view';
-
-const App = new Marionette.Application();
-
-App.on('start', options => {
-  const viewOptions = {
-    ...options,
-    model: new Backbone.Model()
-  };
-  const mainView = new MainView(viewOptions);
-  mainView.render().refresh();
-});
-
-export default function(el, setup, returnTo) {
-  App.start({ el, setup, returnTo });
-}
diff --git a/server/sonar-web/src/main/js/apps/maintenance/main-view.js b/server/sonar-web/src/main/js/apps/maintenance/main-view.js
deleted file mode 100644 (file)
index da55820..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-import Marionette from 'backbone.marionette';
-import Template from './templates/maintenance-main.hbs';
-import { getSystemStatus, getMigrationStatus, migrateDatabase } from '../../api/system';
-import { getBaseUrl } from '../../helpers/urls';
-
-export default Marionette.ItemView.extend({
-  template: Template,
-
-  events: {
-    'click #start-migration': 'startMigration'
-  },
-
-  initialize() {
-    this.pollingInternal = setInterval(() => {
-      this.refresh();
-    }, 5000);
-    this.wasStarting = false;
-  },
-
-  getStatus() {
-    return this.options.setup ? getMigrationStatus() : getSystemStatus();
-  },
-
-  refresh() {
-    return this.getStatus().then(
-      r => {
-        if (r.status === 'STARTING') {
-          this.wasStarting = true;
-        }
-        // unset `status` in case if was `OFFLINE` previously
-        this.model.set({ status: undefined, ...r });
-        this.render();
-        if (this.model.get('status') === 'UP' || this.model.get('state') === 'NO_MIGRATION') {
-          this.stopPolling();
-        }
-        if (this.model.get('status') === 'UP' && this.wasStarting) {
-          this.loadPreviousPage();
-        }
-        if (this.model.get('state') === 'MIGRATION_SUCCEEDED') {
-          this.loadPreviousPage();
-        }
-      },
-      () => {
-        this.model.set({ status: 'OFFLINE' });
-        this.render();
-      }
-    );
-  },
-
-  stopPolling() {
-    clearInterval(this.pollingInternal);
-  },
-
-  startMigration() {
-    migrateDatabase().then(
-      r => {
-        this.model.set(r);
-        this.render();
-      },
-      () => {}
-    );
-  },
-
-  loadPreviousPage() {
-    setInterval(() => {
-      window.location = this.options.returnTo || getBaseUrl();
-    }, 2500);
-  },
-
-  serializeData() {
-    return {
-      ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
-      setup: this.options.setup
-    };
-  }
-});
index 7ef353bbe4a14ab2e6599c84ffd88064c4dfb6ac..ac4c46ec21cc0c7a6f2ded68a3535eb6e5d21813 100644 (file)
  */
 import * as React from 'react';
 import { IndexRoute } from 'react-router';
-import MaintenanceAppContainer from './components/MaintenanceAppContainer';
-import SetupAppContainer from './components/SetupAppContainer';
+import { lazyLoad } from '../../components/lazyLoad';
 
-export const maintenanceRoutes = <IndexRoute component={MaintenanceAppContainer} />;
+export const maintenanceRoutes = (
+  <IndexRoute component={lazyLoad(() => import('./components/MaintenanceAppContainer'))} />
+);
 
-export const setupRoutes = <IndexRoute component={SetupAppContainer} />;
+export const setupRoutes = (
+  <IndexRoute component={lazyLoad(() => import('./components/SetupAppContainer'))} />
+);
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-failed.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-failed.hbs
deleted file mode 100644 (file)
index ab1421b..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-<h1 class="maintenance-title text-danger">{{t 'maintenance.upgrade_failed'}}</h1>
-<p class="maintenance-text">{{t 'maintenance.upgrade_failed.text'}}</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-not-supported.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-not-supported.hbs
deleted file mode 100644 (file)
index 6d02304..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-<h1 class="maintenance-title text-danger">{{t 'maintenance.migration_not_supported'}}</h1>
-<p>{{t 'maintenance.migration_not_supported.text'}}</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-required.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-required.hbs
deleted file mode 100644 (file)
index 3236ff5..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<h1 class="maintenance-title">{{t 'maintenance.upgrade_database'}}</h1>
-<p class="maintenance-text">{{t 'maintenance.upgrade_database.1'}}</p>
-<p class="maintenance-text">{{t 'maintenance.upgrade_database.2'}}</p>
-<p class="maintenance-text">{{t 'maintenance.upgrade_database.3'}}</p>
-<div class="maintenance-spinner">
-  <button id="start-migration">{{t 'maintenance.upgrade'}}</button>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-running.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-running.hbs
deleted file mode 100644 (file)
index c69baef..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<h1 class="maintenance-title">{{t 'maintenance.database_migration'}}</h1>
-{{#if message}}
-  <p class="maintenance-text text-center">{{message}}</p>
-{{/if}}
-{{#if startedAt}}
-  <p class="maintenance-text text-center">
-    {{t 'background_tasks.table.started'}} {{fromNow startedAt}}<br>
-    <small class="text-muted">{{dt startedAt}}</small>
-  </p>
-{{/if}}
-<p class="maintenance-spinner"><i class="spinner"></i></p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-succeeded.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-migration-succeeded.hbs
deleted file mode 100644 (file)
index 7e70e68..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<h1 class="maintenance-title text-success">{{t 'maintenance.database_is_up_to_date'}}</h1>
-<p class="maintenance-text text-center">
-  <a href="{{link '/'}}">{{t 'layout.home'}}</a>
-</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-no-migration.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-state-no-migration.hbs
deleted file mode 100644 (file)
index 6c30631..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<h1 class="maintenance-title">{{t 'maintenance.database_is_up_to_date'}}</h1>
-<p class="maintenance-text text-center">
-  <a href="{{link '/'}}">{{t 'layout.home'}}</a>
-</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-down.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-down.hbs
deleted file mode 100644 (file)
index 811aa16..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<h1 class="maintenance-title text-danger">{{t 'maintenance.sonarqube_is_down'}}</h1>
-<p class="maintenance-text">{{t 'maintenance.sonarqube_is_down.text'}}</p>
-<p class="maintenance-text text-center">
-  <a href="{{link '/'}}">{{t 'maintenance.try_again'}}</a>
-</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-migration.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-migration.hbs
deleted file mode 100644 (file)
index f51f4c7..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<h1 class="maintenance-title">{{t 'maintenance.sonarqube_is_under_maintenance'}}</h1>
-<p class="maintenance-text">{{{t 'maintenance.sonarqube_is_under_maintenance.1'}}}</p>
-<p class="maintenance-text">{{{t 'maintenance.sonarqube_is_under_maintenance.2'}}}</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-offline.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-offline.hbs
deleted file mode 100644 (file)
index 6e7bac3..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<h1 class="maintenance-title text-danger">{{t 'maintenance.sonarqube_is_offline'}}</h1>
-<p class="maintenance-text">{{t 'maintenance.sonarqube_is_offline.text'}}</p>
-<p class="maintenance-text text-center">
-  <a href="{{link '/'}}">{{t 'maintenance.try_again'}}</a>
-</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-starting.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-starting.hbs
deleted file mode 100644 (file)
index 96f7d89..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-<h1 class="maintenance-title">{{t 'maintenance.sonarqube_is_starting'}}</h1>
-<p class="maintenance-spinner"><i class="spinner"></i></p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-up.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/_maintenance-status-up.hbs
deleted file mode 100644 (file)
index e97b550..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<h1 class="maintenance-title">{{t 'maintenance.sonarqube_is_up'}}</h1>
-<p class="maintenance-text text-center">{{t 'maintenance.all_systems_opetational'}}</p>
-<p class="maintenance-text text-center">
-  <a href="{{link '/'}}">{{t 'layout.home'}}</a>
-</p>
diff --git a/server/sonar-web/src/main/js/apps/maintenance/templates/maintenance-main.hbs b/server/sonar-web/src/main/js/apps/maintenance/templates/maintenance-main.hbs
deleted file mode 100644 (file)
index 44a4bc8..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<div id="bd" class="page-wrapper-simple">
-  <div id="nonav" class="page-simple {{#eq state 'MIGRATION_REQUIRED'}}panel-warning{{/eq}}">
-
-    {{#eq status 'OFFLINE'}}
-
-      {{> '_maintenance-status-offline'}}
-
-    {{else}}
-
-      {{#unless setup}}
-
-        {{#eq status 'UP'}}{{> '_maintenance-status-up'}}{{/eq}}
-        {{#eq status 'STARTING'}}{{> '_maintenance-status-starting'}}{{/eq}}
-        {{#eq status 'DOWN'}}{{> '_maintenance-status-down'}}{{/eq}}
-        {{#eq status 'DB_MIGRATION_NEEDED'}}{{> '_maintenance-status-migration'}}{{/eq}}
-        {{#eq status 'DB_MIGRATION_RUNNING'}}{{> '_maintenance-status-migration'}}{{/eq}}
-
-      {{else}}
-
-        {{#eq state 'NO_MIGRATION'}}{{> '_maintenance-state-no-migration'}}{{/eq}}
-        {{#eq state 'MIGRATION_REQUIRED'}}{{> '_maintenance-state-migration-required'}}{{/eq}}
-        {{#eq state 'NOT_SUPPORTED'}}{{> '_maintenance-state-migration-not-supported'}}{{/eq}}
-        {{#eq state 'MIGRATION_RUNNING'}}{{> '_maintenance-state-migration-running'}}{{/eq}}
-        {{#eq state 'MIGRATION_SUCCEEDED'}}{{> '_maintenance-state-migration-succeeded'}}{{/eq}}
-        {{#eq state 'MIGRATION_FAILED'}}{{> '_maintenance-state-migration-failed'}}{{/eq}}
-
-      {{/unless}}
-
-    {{/eq}}
-
-  </div>
-</div>
index fbf8be8d1f775404066818bc957b2a126e239e84..158a52befeda7891f77c1b4c91b339d6cee84993 100644 (file)
@@ -20,7 +20,7 @@
 /* eslint-disable import/order */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import { change } from '../../../../helpers/testUtils';
+import { change, waitAndUpdate } from '../../../../helpers/testUtils';
 import LicenseEditionSet from '../LicenseEditionSet';
 
 jest.mock('../../../../api/marketplace', () => ({
@@ -59,8 +59,7 @@ it('should display correctly', () => {
 
 it('should display the get license link with parameters', async () => {
   const wrapper = getWrapper();
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.find('a')).toMatchSnapshot();
 });
 
index fbf21407ac94a4748ddc25b19063833b58a306dc..da0539059305007972751df0f13afe3f78dace85 100644 (file)
@@ -28,7 +28,7 @@ jest.mock('../../../../api/report', () => {
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import Subscription from '../Subscription';
-import { click } from '../../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../../helpers/testUtils';
 
 const subscribe = require('../../../../api/report').subscribe as jest.Mock<any>;
 const unsubscribe = require('../../../../api/report').unsubscribe as jest.Mock<any>;
@@ -77,8 +77,7 @@ it('changes subscription', async () => {
   click(wrapper.find('button'));
   expect(unsubscribe).toBeCalledWith('foo');
 
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
 
   click(wrapper.find('button'));
   expect(subscribe).toBeCalledWith('foo');
index 50a2baa79a36d6285dd4cee115e415695cb5e2cc..d23beef1c6e36624df35609f979272f4cded43a8 100644 (file)
@@ -24,7 +24,7 @@ import * as React from 'react';
 import { shallow, ShallowWrapper } from 'enzyme';
 import DeleteBranchModal from '../DeleteBranchModal';
 import { ShortLivingBranch, BranchType } from '../../../../app/types';
-import { submit, doAsync, click } from '../../../../helpers/testUtils';
+import { submit, doAsync, click, waitAndUpdate } from '../../../../helpers/testUtils';
 import { deleteBranch } from '../../../../api/branches';
 
 beforeEach(() => {
@@ -38,19 +38,17 @@ it('renders', () => {
   expect(wrapper).toMatchSnapshot();
 });
 
-it('deletes branch', () => {
+it('deletes branch', async () => {
   (deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.resolve());
   const onDelete = jest.fn();
   const wrapper = shallowRender(onDelete);
 
   submitForm(wrapper);
 
-  return doAsync().then(() => {
-    wrapper.update();
-    expect(wrapper.state().loading).toBe(false);
-    expect(onDelete).toBeCalled();
-    expect(deleteBranch).toBeCalledWith('foo', 'feature');
-  });
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+  expect(onDelete).toBeCalled();
+  expect(deleteBranch).toBeCalledWith('foo', 'feature');
 });
 
 it('cancels', () => {
@@ -64,19 +62,17 @@ it('cancels', () => {
   });
 });
 
-it('stops loading on WS error', () => {
+it('stops loading on WS error', async () => {
   (deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.reject(null));
   const onDelete = jest.fn();
   const wrapper = shallowRender(onDelete);
 
   submitForm(wrapper);
 
-  return doAsync().then(() => {
-    wrapper.update();
-    expect(wrapper.state().loading).toBe(false);
-    expect(onDelete).not.toBeCalled();
-    expect(deleteBranch).toBeCalledWith('foo', 'feature');
-  });
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+  expect(onDelete).not.toBeCalled();
+  expect(deleteBranch).toBeCalledWith('foo', 'feature');
 });
 
 function shallowRender(onDelete: () => void = jest.fn(), onClose: () => void = jest.fn()) {
index 1c55b08314fa180be18f921cf0b722b08ce74cfa..3d897c3a9530d6a12bd80b5479be99efa0c0e9fc 100644 (file)
@@ -24,7 +24,7 @@ import * as React from 'react';
 import { shallow, ShallowWrapper } from 'enzyme';
 import RenameBranchModal from '../RenameBranchModal';
 import { MainBranch } from '../../../../app/types';
-import { submit, doAsync, click, change } from '../../../../helpers/testUtils';
+import { submit, doAsync, click, change, waitAndUpdate } from '../../../../helpers/testUtils';
 import { renameBranch } from '../../../../api/branches';
 
 beforeEach(() => {
@@ -40,19 +40,17 @@ it('renders', () => {
   expect(wrapper).toMatchSnapshot();
 });
 
-it('renames branch', () => {
+it('renames branch', async () => {
   (renameBranch as jest.Mock<any>).mockImplementation(() => Promise.resolve());
   const onRename = jest.fn();
   const wrapper = shallowRender(onRename);
 
   fillAndSubmit(wrapper);
 
-  return doAsync().then(() => {
-    wrapper.update();
-    expect(wrapper.state().loading).toBe(false);
-    expect(onRename).toBeCalled();
-    expect(renameBranch).toBeCalledWith('foo', 'dev');
-  });
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+  expect(onRename).toBeCalled();
+  expect(renameBranch).toBeCalledWith('foo', 'dev');
 });
 
 it('cancels', () => {
@@ -66,18 +64,16 @@ it('cancels', () => {
   });
 });
 
-it('stops loading on WS error', () => {
+it('stops loading on WS error', async () => {
   (renameBranch as jest.Mock<any>).mockImplementation(() => Promise.reject(null));
   const onRename = jest.fn();
   const wrapper = shallowRender(onRename);
 
   fillAndSubmit(wrapper);
 
-  return doAsync().then(() => {
-    wrapper.update();
-    expect(wrapper.state().loading).toBe(false);
-    expect(onRename).not.toBeCalled();
-  });
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+  expect(onRename).not.toBeCalled();
 });
 
 function shallowRender(onRename: () => void = jest.fn(), onClose: () => void = jest.fn()) {
index ed9c56dbcb1801d7ec716a148e565bea7506b89a..09c56289581fcaf093f4fa6bd1e8d968997c24e4 100644 (file)
@@ -21,7 +21,7 @@
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import BulkApplyTemplateModal, { Props } from '../BulkApplyTemplateModal';
-import { click } from '../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../helpers/testUtils';
 
 jest.mock('react-dom');
 
@@ -67,8 +67,7 @@ it('bulk applies template to all results', async () => {
   });
   expect(wrapper).toMatchSnapshot();
 
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
index 1cdac62d85e4b5d3c965e31da53fe3f27aebe151..d79aa247497ca9177811c3e85d51ba776fedea6b 100644 (file)
@@ -27,7 +27,7 @@ jest.mock('../../../api/components', () => ({
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import CreateProjectForm from '../CreateProjectForm';
-import { change, submit, click } from '../../../helpers/testUtils';
+import { change, submit, click, waitAndUpdate } from '../../../helpers/testUtils';
 import { Visibility } from '../../../app/types';
 
 const createProject = require('../../../api/components').createProject as jest.Mock<any>;
@@ -65,8 +65,7 @@ it('creates project', async () => {
   });
   expect(wrapper).toMatchSnapshot();
 
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
index a615b0e0c771c7b56bdef6e7ec536fc75f09a4a9..f9a5eee840673039da2fc2ae6d049917ca0066bc 100644 (file)
@@ -21,7 +21,7 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import ProjectRowActions, { Props } from '../ProjectRowActions';
 import { Visibility } from '../../../app/types';
-import { click } from '../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../helpers/testUtils';
 
 jest.mock('../../../api/components', () => ({
   getComponentShow: jest.fn(() => Promise.reject(undefined))
@@ -45,8 +45,7 @@ it('restores access', async () => {
   expect(wrapper).toMatchSnapshot();
 
   wrapper.prop<Function>('onToggleClick')();
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 
   click(wrapper.find('.js-restore-access'));
index 0536833ec18c6b19d87d5c8d973ced3085104ba1..08452e22416f3c0c007d624331f8b5f602c7d997 100644 (file)
@@ -21,7 +21,7 @@
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import ProfilePermissions from '../ProfilePermissions';
-import { click } from '../../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/quality-profiles', () => ({
   searchUsers: jest.fn(() => Promise.resolve([])),
@@ -56,8 +56,7 @@ it('opens add users form', async () => {
   );
   const wrapper = shallow(<ProfilePermissions profile={profile} />);
   expect(searchUsers).toHaveBeenCalled();
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.find('ProfilePermissionsForm').exists()).toBeFalsy();
 
   click(wrapper.find('button'));
index 0f75a3da1e39a6864ede453a8ee6e84c1bf23f01..fcabce9784a291e703165f221728d5c7ca2bcd63 100644 (file)
@@ -22,6 +22,7 @@ import { shallow } from 'enzyme';
 import ProfileRules from '../ProfileRules';
 import * as apiRules from '../../../../api/rules';
 import * as apiQP from '../../../../api/quality-profiles';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
 
 const PROFILE = {
   activeRuleCount: 68,
@@ -86,8 +87,7 @@ it('should render the quality profiles rules with sonarway comparison', async ()
   const instance = wrapper.instance() as any;
   instance.mounted = true;
   instance.loadRules();
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.find('ProfileRulesSonarWayComparison')).toHaveLength(1);
   expect(wrapper).toMatchSnapshot();
 });
@@ -136,8 +136,7 @@ it('should not show sonarway comparison if there is no missing rules', async ()
     })
   );
   const wrapper = shallow(<ProfileRules organization={null} profile={PROFILE} />);
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(apiQP.getQualityProfile).toHaveBeenCalledTimes(1);
   expect(wrapper.find('ProfileRulesSonarWayComparison')).toHaveLength(0);
 });
index f0a4baca93d6b8b73eeadfc2ea420bce4c34c1c0..81b2176c785422d85dd23d20528311f37830ff37 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import EmailAlreadyExists from '../EmailAlreadyExists';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/users', () => ({
   getIdentityProviders: () =>
@@ -52,7 +53,6 @@ it('render', async () => {
   const wrapper = shallow(<EmailAlreadyExists location={{ query }} />);
   (wrapper.instance() as EmailAlreadyExists).mounted = true;
   (wrapper.instance() as EmailAlreadyExists).fetchIdentityProviders();
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
index 49e07c3a824c0b10d7b1a136de7d236878e3dfe0..0ae8979a9d7d1bab72cd4c2de6a1afcad3903304 100644 (file)
@@ -20,7 +20,7 @@
 /* eslint-disable import/order */
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
-import { click } from '../../../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../../../helpers/testUtils';
 import SystemUpgradeNotif from '../SystemUpgradeNotif';
 
 jest.mock('../../../../../api/system', () => ({
@@ -82,8 +82,7 @@ beforeEach(() => {
 it('should display correctly', async () => {
   const wrapper = shallow(<SystemUpgradeNotif />);
   expect(wrapper.type()).toBeNull();
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
@@ -92,8 +91,7 @@ it('should display nothing', async () => {
     return Promise.resolve({ updateCenterRefresh: '', upgrades: [] });
   });
   const wrapper = shallow(<SystemUpgradeNotif />);
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper.type()).toBeNull();
 });
 
@@ -104,8 +102,7 @@ it('should fetch upgrade when mounting', () => {
 
 it('should open the upgrade form', async () => {
   const wrapper = shallow(<SystemUpgradeNotif />);
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   click(wrapper.find('button'));
   expect(wrapper.find('SystemUpgradeForm').exists()).toBeTruthy();
 });
index 3db2d2faaa627fa05a574632fae4ebbfb8849c54..fcfbf8d810c58b6f6cc0699bb7d087c55354a7e7 100644 (file)
@@ -21,7 +21,7 @@
 import React from 'react';
 import { mount } from 'enzyme';
 import NewOrganizationForm from '../NewOrganizationForm';
-import { change, submit } from '../../../../helpers/testUtils';
+import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/organizations', () => ({
   createOrganization: () => Promise.resolve(),
@@ -38,8 +38,7 @@ it('creates new organization', async () => {
   change(wrapper.find('input'), 'foo');
   submit(wrapper.find('form'));
   expect(wrapper).toMatchSnapshot(); // spinner
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
   expect(onDone).toBeCalledWith('foo');
 });
@@ -52,8 +51,7 @@ it('deletes organization', async () => {
   wrapper.find('DeleteButton').prop('onClick')();
   wrapper.update();
   expect(wrapper).toMatchSnapshot(); // spinner
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
   expect(onDelete).toBeCalled();
 });
index 24f4669106fe9043a619b9a9ce0c1f6810ca13a6..2d290f10503808319af64418a51ada5e470f4f9a 100644 (file)
@@ -21,7 +21,7 @@
 import React from 'react';
 import { mount } from 'enzyme';
 import NewProjectForm from '../NewProjectForm';
-import { change, submit } from '../../../../helpers/testUtils';
+import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/components', () => ({
   createProject: () => Promise.resolve(),
@@ -37,8 +37,7 @@ it('creates new project', async () => {
   change(wrapper.find('input'), 'foo');
   submit(wrapper.find('form'));
   expect(wrapper).toMatchSnapshot(); // spinner
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
   expect(onDone).toBeCalledWith('foo');
 });
@@ -51,8 +50,7 @@ it('deletes project', async () => {
   wrapper.find('DeleteButton').prop('onClick')();
   wrapper.update();
   expect(wrapper).toMatchSnapshot(); // spinner
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
   expect(onDelete).toBeCalled();
 });
index d284ebc04613409ec16afb156e1ebb54861940c1..01add62b5391b533513b0e358e66cb207b77539c 100644 (file)
@@ -21,7 +21,7 @@
 import React from 'react';
 import { mount } from 'enzyme';
 import OrganizationStep from '../OrganizationStep';
-import { click } from '../../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../../helpers/testUtils';
 import { getOrganizations } from '../../../../api/organizations';
 
 jest.mock('../../../../api/organizations', () => ({
@@ -70,8 +70,7 @@ it('works with existing organization', async () => {
       stepNumber={1}
     />
   );
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   click(wrapper.find('.js-existing'));
   expect(wrapper).toMatchSnapshot();
   wrapper
@@ -95,8 +94,7 @@ it('works with new organization', async () => {
       stepNumber={1}
     />
   );
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   click(wrapper.find('.js-new'));
   wrapper.find('NewOrganizationForm').prop('onDone')('new');
   wrapper.update();
index c2fc31a6ab7791fee880d9484a405d27e1594fdb..d336accbca714859c67f2632b2c01e336af8a53c 100644 (file)
@@ -21,7 +21,7 @@
 import React from 'react';
 import { mount } from 'enzyme';
 import TokenStep from '../TokenStep';
-import { change, click, submit } from '../../../../helpers/testUtils';
+import { change, click, submit, waitAndUpdate } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/user-tokens', () => ({
   getTokens: () => Promise.resolve([{ name: 'foo' }]),
@@ -44,14 +44,12 @@ it('generates token', async () => {
       stepNumber={1}
     />
   );
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
   change(wrapper.find('input'), 'my token');
   submit(wrapper.find('form'));
   expect(wrapper).toMatchSnapshot(); // spinner
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
@@ -72,8 +70,7 @@ it('revokes token', async () => {
   wrapper.find('DeleteButton').prop('onClick')();
   wrapper.update();
   expect(wrapper).toMatchSnapshot(); // spinner
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
index 1c059f39f314f69b204180fc3fd17da9c9e1a7d7..5adb4d2dae6a8d19ba401aba3704cf2857eb779c 100644 (file)
@@ -22,6 +22,7 @@ import * as React from 'react';
 import { Location } from 'history';
 import { shallow } from 'enzyme';
 import UsersApp from '../UsersApp';
+import { waitAndUpdate } from '../../../helpers/testUtils';
 
 jest.mock('../../../api/users', () => ({
   getIdentityProviders: jest.fn(() =>
@@ -72,8 +73,7 @@ it('should render correctly', async () => {
   expect(wrapper).toMatchSnapshot();
   expect(getIdentityProviders).toHaveBeenCalled();
   expect(searchUsers).toHaveBeenCalled();
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
index 32d388fa739527ab985b8877cec6f998be0c756a..c4ea074996ca3e8dd1a1784e1a036fb413b0bf2c 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import SimpleModal, { ChildrenProps } from '../SimpleModal';
-import { click } from '../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../helpers/testUtils';
 
 it('renders', () => {
   const inner = () => <div />;
@@ -64,7 +64,6 @@ it('submits', async () => {
   expect(onSubmit).toBeCalled();
   expect(wrapper).toMatchSnapshot();
 
-  await new Promise(setImmediate);
-  wrapper.update();
+  await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
index f12b4734b6fa7b87a5b265ef86574eca92232152..34f6de91d6be07b72cd40114f5431a86e624efd6 100644 (file)
  */
 import * as React from 'react';
 
-interface Loader {
-  (): Promise<{ default: React.ComponentClass }>;
+type ReactComponent<P> = React.ComponentClass<P> | React.StatelessComponent<P>;
+
+interface Loader<P> {
+  (): Promise<{ default: ReactComponent<P> }>;
 }
 
-export function lazyLoad(loader: Loader) {
+export function lazyLoad<P>(loader: Loader<P>) {
   interface State {
-    Component?: React.ComponentClass;
+    Component?: ReactComponent<P>;
   }
 
   // use `React.Component`, not `React.PureComponent` to always re-render
@@ -43,7 +45,7 @@ export function lazyLoad(loader: Loader) {
       this.mounted = false;
     }
 
-    receiveComponent = (Component: React.ComponentClass) => {
+    receiveComponent = (Component: ReactComponent<P>) => {
       if (this.mounted) {
         this.setState({ Component });
       }
index 25730a2e4b128f1926635a22c94f43d885b09eb6..5e7ad5f1d4cafcd5664c5be8bc74b87539f4b40e 100644 (file)
@@ -87,3 +87,8 @@ const { intl } = intlProvider.getChildContext();
 export function shallowWithIntl(node: React.ReactElement<any>, options: ShallowRendererProps = {}) {
   return shallow(node, { ...options, context: { intl, ...options.context } });
 }
+
+export async function waitAndUpdate(wrapper: ShallowWrapper<any, any> | ReactWrapper<any, any>) {
+  await new Promise(setImmediate);
+  wrapper.update();
+}