]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite the rest of the background tasks app in ts
authorStas Vilchik <stas.vilchik@sonarsource.com>
Thu, 30 Aug 2018 12:10:47 +0000 (14:10 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 10 Oct 2018 07:23:03 +0000 (09:23 +0200)
18 files changed:
server/sonar-web/src/main/js/api/ce.ts
server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/components/Search.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/components/StatsContainer.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/Task.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/constants.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/constants.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/utils.js [deleted file]
server/sonar-web/src/main/js/apps/background-tasks/utils.ts [new file with mode: 0644]

index 3d61fbc6589287dc2c7fece3643eb3273212a02e..3762ec76e1c3e5d8ff16e051c45e571fc1c09316 100644 (file)
@@ -21,11 +21,13 @@ import { getJSON, post, RequestData } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
 import { Task } from '../app/types';
 
-export function getActivity(data: RequestData): Promise<any> {
+export function getActivity(data: RequestData): Promise<{ tasks: Task[] }> {
   return getJSON('/api/ce/activity', data);
 }
 
-export function getStatus(componentId?: string): Promise<any> {
+export function getStatus(
+  componentId?: string
+): Promise<{ failing: number; inProgress: number; pending: number }> {
   const data = {};
   if (componentId) {
     Object.assign(data, { componentId });
@@ -51,7 +53,7 @@ export function getTasksForComponent(
   return getJSON('/api/ce/component', { componentKey }).catch(throwGlobalError);
 }
 
-export function getTypes(): Promise<any> {
+export function getTypes(): Promise<string[]> {
   return getJSON('/api/ce/task_types').then(r => r.taskTypes);
 }
 
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-test.tsx b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-test.tsx
new file mode 100644 (file)
index 0000000..8dfe216
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import Stats from '../components/Stats';
+import Search from '../components/Search';
+import { STATUSES, CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS } from '../constants';
+import { formatDuration } from '../utils';
+import { click } from '../../../helpers/testUtils';
+
+const stub = jest.fn();
+
+describe('Constants', () => {
+  it('should have STATUSES', () => {
+    expect(Object.keys(STATUSES).length).toBe(7);
+  });
+
+  it('should have CURRENTS', () => {
+    expect(Object.keys(CURRENTS).length).toBe(2);
+  });
+});
+
+describe('Search', () => {
+  const defaultProps: Search['props'] = {
+    ...DEFAULT_FILTERS,
+    loading: false,
+    types: [],
+    onFilterUpdate: () => true,
+    onReload: () => true,
+    maxExecutedAt: undefined,
+    minSubmittedAt: undefined
+  };
+
+  it('should render search form', () => {
+    const component = shallow(<Search {...defaultProps} />);
+    expect(component.find('SearchBox').exists()).toBeTruthy();
+  });
+
+  it('should not render search form', () => {
+    const component = shallow(<Search {...defaultProps} component={{ id: 'ABCD' }} />);
+    expect(component.find('SearchBox').exists()).toBeFalsy();
+  });
+
+  it('should search', done => {
+    const searchSpy = jest.fn();
+    const component = shallow(<Search {...defaultProps} onFilterUpdate={searchSpy} />);
+    const searchInput = component.find('SearchBox');
+    searchInput.prop<Function>('onChange')('some search query');
+    setTimeout(() => {
+      expect(searchSpy).toBeCalledWith({ query: 'some search query' });
+      done();
+    }, DEBOUNCE_DELAY);
+  });
+
+  it('should reload', () => {
+    const reloadSpy = jest.fn();
+    const component = shallow(<Search {...defaultProps} onReload={reloadSpy} />);
+    const reloadButton = component.find('.js-reload');
+    expect(reloadSpy).not.toBeCalled();
+    click(reloadButton);
+    expect(reloadSpy).toBeCalled();
+  });
+});
+
+describe('Stats', () => {
+  describe('Pending', () => {
+    it('should show zero pending', () => {
+      const result = shallow(
+        <Stats onCancelAllPending={stub} onShowFailing={stub} pendingCount={0} />
+      );
+      expect(result.find('.js-pending-count').text()).toContain('0');
+    });
+
+    it('should show 5 pending', () => {
+      const result = shallow(
+        <Stats onCancelAllPending={stub} onShowFailing={stub} pendingCount={5} />
+      );
+      expect(result.find('.js-pending-count').text()).toContain('5');
+    });
+
+    it('should not show cancel pending button', () => {
+      const result = shallow(
+        <Stats onCancelAllPending={stub} onShowFailing={stub} pendingCount={0} />
+      );
+      expect(result.find('.js-cancel-pending').length).toBe(0);
+    });
+
+    it('should show cancel pending button', () => {
+      const result = shallow(
+        <Stats
+          isSystemAdmin={true}
+          onCancelAllPending={stub}
+          onShowFailing={stub}
+          pendingCount={5}
+        />
+      );
+      expect(result.find('.js-cancel-pending').length).toBe(1);
+    });
+
+    it('should trigger cancelling pending', () => {
+      const spy = jest.fn();
+      const result = shallow(
+        <Stats
+          isSystemAdmin={true}
+          onCancelAllPending={spy}
+          onShowFailing={stub}
+          pendingCount={5}
+        />
+      );
+      expect(spy).not.toBeCalled();
+      click(result.find('.js-cancel-pending'));
+      expect(spy).toBeCalled();
+    });
+  });
+
+  describe('Failures', () => {
+    it('should show zero failures', () => {
+      const result = shallow(
+        <Stats failingCount={0} onCancelAllPending={stub} onShowFailing={stub} />
+      );
+      expect(result.find('.js-failures-count').text()).toContain('0');
+    });
+
+    it('should show 5 failures', () => {
+      const result = shallow(
+        <Stats failingCount={5} onCancelAllPending={stub} onShowFailing={stub} />
+      );
+      expect(result.find('.js-failures-count').text()).toContain('5');
+    });
+
+    it('should not show link to failures', () => {
+      const result = shallow(
+        <Stats failingCount={0} onCancelAllPending={stub} onShowFailing={stub} />
+      );
+      expect(result.find('.js-failures-count').is('a')).toBeFalsy();
+    });
+
+    it('should show link to failures', () => {
+      const result = shallow(
+        <Stats failingCount={5} onCancelAllPending={stub} onShowFailing={stub} />
+      );
+      expect(result.find('.js-failures-count').is('a')).toBeTruthy();
+    });
+
+    it('should trigger filtering failures', () => {
+      const spy = jest.fn();
+      const result = shallow(
+        <Stats failingCount={5} onCancelAllPending={stub} onShowFailing={spy} />
+      );
+      expect(spy).not.toBeCalled();
+      click(result.find('.js-failures-count'));
+      expect(spy).toBeCalled();
+    });
+  });
+});
+
+describe('Helpers', () => {
+  describe('#formatDuration()', () => {
+    it('should format 173ms', () => {
+      expect(formatDuration(173)).toBe('173ms');
+    });
+
+    it('should format 999ms', () => {
+      expect(formatDuration(999)).toBe('999ms');
+    });
+
+    it('should format 1s 0ms', () => {
+      expect(formatDuration(1000)).toBe('1.0s');
+    });
+
+    it('should format 1s 1ms', () => {
+      expect(formatDuration(1001)).toBe('1.1s');
+    });
+
+    it('should format 1s 501ms', () => {
+      expect(formatDuration(1501)).toBe('1.501s');
+    });
+
+    it('should format 59s', () => {
+      expect(formatDuration(59000)).toBe('59s');
+    });
+
+    it('should format 1min 0s', () => {
+      expect(formatDuration(60000)).toBe('1min 0s');
+    });
+
+    it('should format 1min 2s', () => {
+      expect(formatDuration(62757)).toBe('1min 2s');
+    });
+
+    it('should format 3min 44s', () => {
+      expect(formatDuration(224567)).toBe('3min 44s');
+    });
+
+    it('should format 1h 20m', () => {
+      expect(formatDuration(80 * 60 * 1000)).toBe('1h 20min');
+    });
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js
deleted file mode 100644 (file)
index f4f686f..0000000
+++ /dev/null
@@ -1,214 +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 React from 'react';
-import { shallow } from 'enzyme';
-import Stats from '../components/Stats';
-import Search from '../components/Search';
-import { STATUSES, CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS } from '../constants';
-import { formatDuration } from '../utils';
-import { change, click } from '../../../helpers/testUtils';
-
-const stub = jest.fn();
-
-describe('Constants', () => {
-  it('should have STATUSES', () => {
-    expect(Object.keys(STATUSES).length).toBe(7);
-  });
-
-  it('should have CURRENTS', () => {
-    expect(Object.keys(CURRENTS).length).toBe(2);
-  });
-});
-
-describe('Search', () => {
-  const defaultProps = {
-    ...DEFAULT_FILTERS,
-    loading: false,
-    types: [],
-    onFilterUpdate: () => true,
-    onReload: () => true
-  };
-
-  it('should render search form', () => {
-    const component = shallow(<Search {...defaultProps} />);
-    expect(component.find('SearchBox').exists()).toBeTruthy();
-  });
-
-  it('should not render search form', () => {
-    const component = shallow(<Search {...defaultProps} component={{ id: 'ABCD' }} />);
-    expect(component.find('SearchBox').exists()).toBeFalsy();
-  });
-
-  it('should search', done => {
-    const searchSpy = jest.fn();
-    const component = shallow(<Search {...defaultProps} onFilterUpdate={searchSpy} />);
-    const searchInput = component.find('SearchBox');
-    searchInput.prop('onChange')('some search query');
-    setTimeout(() => {
-      expect(searchSpy).toBeCalledWith({ query: 'some search query' });
-      done();
-    }, DEBOUNCE_DELAY);
-  });
-
-  it('should reload', () => {
-    const reloadSpy = jest.fn();
-    const component = shallow(<Search {...defaultProps} onReload={reloadSpy} />);
-    const reloadButton = component.find('.js-reload');
-    expect(reloadSpy).not.toBeCalled();
-    click(reloadButton);
-    expect(reloadSpy).toBeCalled();
-  });
-});
-
-describe('Stats', () => {
-  describe('Pending', () => {
-    it('should show zero pending', () => {
-      const result = shallow(
-        <Stats onCancelAllPending={stub} onShowFailing={stub} pendingCount={0} />
-      );
-      expect(result.find('.js-pending-count').text()).toContain('0');
-    });
-
-    it('should show 5 pending', () => {
-      const result = shallow(
-        <Stats onCancelAllPending={stub} onShowFailing={stub} pendingCount={5} />
-      );
-      expect(result.find('.js-pending-count').text()).toContain('5');
-    });
-
-    it('should not show cancel pending button', () => {
-      const result = shallow(
-        <Stats onCancelAllPending={stub} onShowFailing={stub} pendingCount={0} />
-      );
-      expect(result.find('.js-cancel-pending').length).toBe(0);
-    });
-
-    it('should show cancel pending button', () => {
-      const result = shallow(
-        <Stats
-          isSystemAdmin={true}
-          onCancelAllPending={stub}
-          onShowFailing={stub}
-          pendingCount={5}
-        />
-      );
-      expect(result.find('.js-cancel-pending').length).toBe(1);
-    });
-
-    it('should trigger cancelling pending', () => {
-      const spy = jest.fn();
-      const result = shallow(
-        <Stats
-          isSystemAdmin={true}
-          onCancelAllPending={spy}
-          onShowFailing={stub}
-          pendingCount={5}
-        />
-      );
-      expect(spy).not.toBeCalled();
-      click(result.find('.js-cancel-pending'));
-      expect(spy).toBeCalled();
-    });
-  });
-
-  describe('Failures', () => {
-    it('should show zero failures', () => {
-      const result = shallow(
-        <Stats failingCount={0} onCancelAllPending={stub} onShowFailing={stub} />
-      );
-      expect(result.find('.js-failures-count').text()).toContain('0');
-    });
-
-    it('should show 5 failures', () => {
-      const result = shallow(
-        <Stats failingCount={5} onCancelAllPending={stub} onShowFailing={stub} />
-      );
-      expect(result.find('.js-failures-count').text()).toContain('5');
-    });
-
-    it('should not show link to failures', () => {
-      const result = shallow(
-        <Stats failingCount={0} onCancelAllPending={stub} onShowFailing={stub} />
-      );
-      expect(result.find('.js-failures-count').is('a')).toBeFalsy();
-    });
-
-    it('should show link to failures', () => {
-      const result = shallow(
-        <Stats failingCount={5} onCancelAllPending={stub} onShowFailing={stub} />
-      );
-      expect(result.find('.js-failures-count').is('a')).toBeTruthy();
-    });
-
-    it('should trigger filtering failures', () => {
-      const spy = jest.fn();
-      const result = shallow(
-        <Stats failingCount={5} onCancelAllPending={stub} onShowFailing={spy} />
-      );
-      expect(spy).not.toBeCalled();
-      click(result.find('.js-failures-count'));
-      expect(spy).toBeCalled();
-    });
-  });
-});
-
-describe('Helpers', () => {
-  describe('#formatDuration()', () => {
-    it('should format 173ms', () => {
-      expect(formatDuration(173)).toBe('173ms');
-    });
-
-    it('should format 999ms', () => {
-      expect(formatDuration(999)).toBe('999ms');
-    });
-
-    it('should format 1s 0ms', () => {
-      expect(formatDuration(1000)).toBe('1.0s');
-    });
-
-    it('should format 1s 1ms', () => {
-      expect(formatDuration(1001)).toBe('1.1s');
-    });
-
-    it('should format 1s 501ms', () => {
-      expect(formatDuration(1501)).toBe('1.501s');
-    });
-
-    it('should format 59s', () => {
-      expect(formatDuration(59000)).toBe('59s');
-    });
-
-    it('should format 1min 0s', () => {
-      expect(formatDuration(60000)).toBe('1min 0s');
-    });
-
-    it('should format 1min 2s', () => {
-      expect(formatDuration(62757)).toBe('1min 2s');
-    });
-
-    it('should format 3min 44s', () => {
-      expect(formatDuration(224567)).toBe('3min 44s');
-    });
-
-    it('should format 1h 20m', () => {
-      expect(formatDuration(80 * 60 * 1000)).toBe('1h 20min');
-    });
-  });
-});
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
deleted file mode 100644 (file)
index 50b47f8..0000000
+++ /dev/null
@@ -1,275 +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.
- */
-// @flow
-import React from 'react';
-import PropTypes from 'prop-types';
-import Helmet from 'react-helmet';
-import { debounce, uniq } from 'lodash';
-import { connect } from 'react-redux';
-import Header from './Header';
-import Footer from './Footer';
-import StatsContainer from './StatsContainer';
-import Search from './Search';
-import Tasks from './Tasks';
-import { DEFAULT_FILTERS, DEBOUNCE_DELAY, STATUSES, CURRENTS } from '../constants';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import {
-  getTypes,
-  getActivity,
-  getStatus,
-  cancelAllTasks,
-  cancelTask as cancelTaskAPI
-} from '../../../api/ce';
-import { updateTask, mapFiltersToParameters } from '../utils';
-import { fetchOrganizations } from '../../../store/rootActions';
-import { translate } from '../../../helpers/l10n';
-import { parseAsDate } from '../../../helpers/query';
-import { toShortNotSoISOString } from '../../../helpers/dates';
-import '../background-tasks.css';
-
-/*::
-type Props = {
-  component: Object,
-  location: Object,
-  fetchOrganizations: (Array<string>) => string
-};
-*/
-
-/*::
-type State = {
-  loading: boolean,
-  tasks: Array<*>,
-  types?: Array<*>,
-  query: string,
-  pendingCount: number,
-  failingCount: number
-};
-*/
-
-class BackgroundTasksApp extends React.PureComponent {
-  /*:: loadTasksDebounced: Function; */
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-
-  static contextTypes = {
-    router: PropTypes.object.isRequired
-  };
-
-  state /*: State */ = {
-    loading: true,
-    tasks: [],
-
-    // filters
-    query: '',
-
-    // stats
-    pendingCount: 0,
-    failingCount: 0
-  };
-
-  componentWillMount() {
-    this.loadTasksDebounced = debounce(this.loadTasks.bind(this), DEBOUNCE_DELAY);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-
-    getTypes().then(types => {
-      this.setState({ types });
-      this.loadTasks();
-    });
-  }
-
-  componentDidUpdate(prevProps /*: Props */) {
-    if (
-      prevProps.component !== this.props.component ||
-      prevProps.location !== this.props.location
-    ) {
-      this.loadTasksDebounced();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  loadTasks() {
-    this.setState({ loading: true });
-
-    const status = this.props.location.query.status || DEFAULT_FILTERS.status;
-    const taskType = this.props.location.query.taskType || DEFAULT_FILTERS.taskType;
-    const currents = this.props.location.query.currents || DEFAULT_FILTERS.currents;
-    const minSubmittedAt =
-      this.props.location.query.minSubmittedAt || DEFAULT_FILTERS.minSubmittedAt;
-    const maxExecutedAt = this.props.location.query.maxExecutedAt || DEFAULT_FILTERS.maxExecutedAt;
-    const query = this.props.location.query.query || DEFAULT_FILTERS.query;
-
-    const filters = { status, taskType, currents, minSubmittedAt, maxExecutedAt, query };
-    const parameters /*: Object */ = mapFiltersToParameters(filters);
-
-    if (this.props.component) {
-      parameters.componentId = this.props.component.id;
-    }
-
-    Promise.all([getActivity(parameters), getStatus(parameters.componentId)]).then(responses => {
-      if (this.mounted) {
-        const [activity, status] = responses;
-        const tasks = activity.tasks;
-
-        const pendingCount = status.pending;
-        const failingCount = status.failing;
-
-        const organizations = uniq(tasks.map(task => task.organization).filter(o => o));
-        this.props.fetchOrganizations(organizations);
-
-        this.setState({
-          tasks,
-          pendingCount,
-          failingCount,
-          loading: false
-        });
-      }
-    });
-  }
-
-  handleFilterUpdate(nextState /*: Object */) {
-    const nextQuery = { ...this.props.location.query, ...nextState };
-
-    // remove defaults
-    Object.keys(DEFAULT_FILTERS).forEach(key => {
-      if (nextQuery[key] === DEFAULT_FILTERS[key]) {
-        delete nextQuery[key];
-      }
-    });
-
-    if (nextQuery.minSubmittedAt) {
-      nextQuery.minSubmittedAt = toShortNotSoISOString(nextQuery.minSubmittedAt);
-    }
-
-    if (nextQuery.maxExecutedAt) {
-      nextQuery.maxExecutedAt = toShortNotSoISOString(nextQuery.maxExecutedAt);
-    }
-
-    this.context.router.push({
-      pathname: this.props.location.pathname,
-      query: nextQuery
-    });
-  }
-
-  handleCancelTask(task) {
-    this.setState({ loading: true });
-
-    cancelTaskAPI(task.id).then(nextTask => {
-      if (this.mounted) {
-        const tasks = updateTask(this.state.tasks, nextTask);
-        this.setState({ tasks, loading: false });
-      }
-    });
-  }
-
-  handleFilterTask(task) {
-    this.handleFilterUpdate({ query: task.componentKey });
-  }
-
-  handleShowFailing() {
-    this.handleFilterUpdate({
-      ...DEFAULT_FILTERS,
-      status: STATUSES.FAILED,
-      currents: CURRENTS.ONLY_CURRENTS
-    });
-  }
-
-  handleCancelAllPending() {
-    this.setState({ loading: true });
-
-    cancelAllTasks().then(() => {
-      if (this.mounted) {
-        this.loadTasks();
-      }
-    });
-  }
-
-  render() {
-    const { component } = this.props;
-    const { loading, types, tasks, pendingCount, failingCount } = this.state;
-
-    if (!types) {
-      return (
-        <div className="page page-limited">
-          <i className="spinner" />
-        </div>
-      );
-    }
-
-    const status = this.props.location.query.status || DEFAULT_FILTERS.status;
-    const taskType = this.props.location.query.taskType || DEFAULT_FILTERS.taskType;
-    const currents = this.props.location.query.currents || DEFAULT_FILTERS.currents;
-    const minSubmittedAt = parseAsDate(this.props.location.query.minSubmittedAt);
-    const maxExecutedAt = parseAsDate(this.props.location.query.maxExecutedAt);
-    const query = this.props.location.query.query || '';
-
-    return (
-      <div className="page page-limited">
-        <Suggestions suggestions="background_tasks" />
-        <Helmet title={translate('background_tasks.page')} />
-        <Header component={component} />
-
-        <StatsContainer
-          component={component}
-          failingCount={failingCount}
-          onCancelAllPending={this.handleCancelAllPending.bind(this)}
-          onShowFailing={this.handleShowFailing.bind(this)}
-          pendingCount={pendingCount}
-        />
-
-        <Search
-          component={component}
-          currents={currents}
-          loading={loading}
-          maxExecutedAt={maxExecutedAt}
-          minSubmittedAt={minSubmittedAt}
-          onFilterUpdate={this.handleFilterUpdate.bind(this)}
-          onReload={this.loadTasksDebounced}
-          query={query}
-          status={status}
-          taskType={taskType}
-          types={types}
-        />
-
-        <Tasks
-          component={component}
-          loading={loading}
-          onCancelTask={this.handleCancelTask.bind(this)}
-          onFilterTask={this.handleFilterTask.bind(this)}
-          tasks={tasks}
-        />
-
-        <Footer tasks={tasks} />
-      </div>
-    );
-  }
-}
-
-const mapDispatchToProps = { fetchOrganizations };
-
-export default connect(
-  null,
-  mapDispatchToProps
-)(BackgroundTasksApp);
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx
new file mode 100644 (file)
index 0000000..9dcbfda
--- /dev/null
@@ -0,0 +1,275 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import { debounce, uniq } from 'lodash';
+import { connect } from 'react-redux';
+import { InjectedRouter } from 'react-router';
+import { Location } from 'history';
+import Header from './Header';
+import Footer from './Footer';
+import StatsContainer from './StatsContainer';
+import Search from './Search';
+import Tasks from './Tasks';
+import { DEFAULT_FILTERS, DEBOUNCE_DELAY, STATUSES, CURRENTS } from '../constants';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import {
+  getTypes,
+  getActivity,
+  getStatus,
+  cancelAllTasks,
+  cancelTask as cancelTaskAPI
+} from '../../../api/ce';
+import { updateTask, mapFiltersToParameters, Query } from '../utils';
+import { fetchOrganizations } from '../../../store/rootActions';
+import { translate } from '../../../helpers/l10n';
+import { parseAsDate } from '../../../helpers/query';
+import { toShortNotSoISOString } from '../../../helpers/dates';
+import '../background-tasks.css';
+import { Task } from '../../../app/types';
+
+interface Props {
+  component?: { id: string };
+  fetchOrganizations: (keys: string[]) => void;
+  location: Location;
+  router: Pick<InjectedRouter, 'push'>;
+}
+
+interface State {
+  loading: boolean;
+  tasks: Task[];
+  types?: string[];
+  query: string;
+  pendingCount: number;
+  failingCount: number;
+}
+
+class BackgroundTasksApp extends React.PureComponent<Props, State> {
+  loadTasksDebounced: () => void;
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      failingCount: 0,
+      loading: true,
+      pendingCount: 0,
+      query: '',
+      tasks: []
+    };
+    this.loadTasksDebounced = debounce(this.loadTasks, DEBOUNCE_DELAY);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+
+    getTypes().then(
+      types => {
+        this.setState({ types });
+        this.loadTasks();
+      },
+      () => {}
+    );
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      prevProps.component !== this.props.component ||
+      prevProps.location !== this.props.location
+    ) {
+      this.loadTasksDebounced();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  loadTasks = () => {
+    this.setState({ loading: true });
+
+    const status = this.props.location.query.status || DEFAULT_FILTERS.status;
+    const taskType = this.props.location.query.taskType || DEFAULT_FILTERS.taskType;
+    const currents = this.props.location.query.currents || DEFAULT_FILTERS.currents;
+    const minSubmittedAt =
+      this.props.location.query.minSubmittedAt || DEFAULT_FILTERS.minSubmittedAt;
+    const maxExecutedAt = this.props.location.query.maxExecutedAt || DEFAULT_FILTERS.maxExecutedAt;
+    const query = this.props.location.query.query || DEFAULT_FILTERS.query;
+
+    const filters = { status, taskType, currents, minSubmittedAt, maxExecutedAt, query };
+    const parameters /*: Object */ = mapFiltersToParameters(filters);
+
+    if (this.props.component) {
+      parameters.componentId = this.props.component.id;
+    }
+
+    Promise.all([getActivity(parameters), getStatus(parameters.componentId)]).then(responses => {
+      if (this.mounted) {
+        const [activity, status] = responses;
+        const { tasks } = activity;
+
+        const pendingCount = status.pending;
+        const failingCount = status.failing;
+
+        const organizations = uniq(tasks.map(task => task.organization).filter(o => o));
+        this.props.fetchOrganizations(organizations);
+
+        this.setState({
+          tasks,
+          pendingCount,
+          failingCount,
+          loading: false
+        });
+      }
+    }, this.stopLoading);
+  };
+
+  handleFilterUpdate = (nextState: Partial<Query>) => {
+    const nextQuery = { ...this.props.location.query, ...nextState };
+
+    // remove defaults
+    Object.keys(DEFAULT_FILTERS).forEach((key: keyof typeof DEFAULT_FILTERS) => {
+      if (nextQuery[key] === DEFAULT_FILTERS[key]) {
+        delete nextQuery[key];
+      }
+    });
+
+    if (nextQuery.minSubmittedAt) {
+      nextQuery.minSubmittedAt = toShortNotSoISOString(nextQuery.minSubmittedAt);
+    }
+
+    if (nextQuery.maxExecutedAt) {
+      nextQuery.maxExecutedAt = toShortNotSoISOString(nextQuery.maxExecutedAt);
+    }
+
+    this.props.router.push({
+      pathname: this.props.location.pathname,
+      query: nextQuery
+    });
+  };
+
+  handleCancelTask = (task: Task) => {
+    this.setState({ loading: true });
+
+    cancelTaskAPI(task.id).then(nextTask => {
+      if (this.mounted) {
+        this.setState(state => ({
+          tasks: updateTask(state.tasks, nextTask),
+          loading: false
+        }));
+      }
+    }, this.stopLoading);
+  };
+
+  handleFilterTask = (task: Task) => {
+    this.handleFilterUpdate({ query: task.componentKey });
+  };
+
+  handleShowFailing() {
+    this.handleFilterUpdate({
+      ...DEFAULT_FILTERS,
+      status: STATUSES.FAILED,
+      currents: CURRENTS.ONLY_CURRENTS
+    });
+  }
+
+  handleCancelAllPending() {
+    this.setState({ loading: true });
+
+    cancelAllTasks().then(() => {
+      if (this.mounted) {
+        this.loadTasks();
+      }
+    }, this.stopLoading);
+  }
+
+  render() {
+    const { component } = this.props;
+    const { loading, types, tasks, pendingCount, failingCount } = this.state;
+
+    if (!types) {
+      return (
+        <div className="page page-limited">
+          <i className="spinner" />
+        </div>
+      );
+    }
+
+    const status = this.props.location.query.status || DEFAULT_FILTERS.status;
+    const taskType = this.props.location.query.taskType || DEFAULT_FILTERS.taskType;
+    const currents = this.props.location.query.currents || DEFAULT_FILTERS.currents;
+    const minSubmittedAt = parseAsDate(this.props.location.query.minSubmittedAt);
+    const maxExecutedAt = parseAsDate(this.props.location.query.maxExecutedAt);
+    const query = this.props.location.query.query || '';
+
+    return (
+      <div className="page page-limited">
+        <Suggestions suggestions="background_tasks" />
+        <Helmet title={translate('background_tasks.page')} />
+        <Header component={component} />
+
+        <StatsContainer
+          component={component}
+          failingCount={failingCount}
+          onCancelAllPending={this.handleCancelAllPending}
+          onShowFailing={this.handleShowFailing}
+          pendingCount={pendingCount}
+        />
+
+        <Search
+          component={component}
+          currents={currents}
+          loading={loading}
+          maxExecutedAt={maxExecutedAt}
+          minSubmittedAt={minSubmittedAt}
+          onFilterUpdate={this.handleFilterUpdate}
+          onReload={this.loadTasksDebounced}
+          query={query}
+          status={status}
+          taskType={taskType}
+          types={types}
+        />
+
+        <Tasks
+          component={component}
+          loading={loading}
+          onCancelTask={this.handleCancelTask}
+          onFilterTask={this.handleFilterTask}
+          tasks={tasks}
+        />
+
+        <Footer tasks={tasks} />
+      </div>
+    );
+  }
+}
+
+const mapDispatchToProps = { fetchOrganizations };
+
+export default connect(
+  null,
+  mapDispatchToProps
+)(BackgroundTasksApp);
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Search.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Search.js
deleted file mode 100644 (file)
index 3e02fcc..0000000
+++ /dev/null
@@ -1,142 +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.
- */
-/* @flow */
-import React from 'react';
-import PropTypes from 'prop-types';
-import StatusFilter from './StatusFilter';
-import TypesFilter from './TypesFilter';
-import CurrentsFilter from './CurrentsFilter';
-import DateFilter from './DateFilter';
-import { DEFAULT_FILTERS } from '../constants';
-import SearchBox from '../../../components/controls/SearchBox';
-import { Button } from '../../../components/ui/buttons';
-import { translate } from '../../../helpers/l10n';
-
-export default class Search extends React.PureComponent {
-  static propTypes = {
-    loading: PropTypes.bool.isRequired,
-    status: PropTypes.any.isRequired,
-    taskType: PropTypes.any.isRequired,
-    currents: PropTypes.any.isRequired,
-    query: PropTypes.string.isRequired,
-    onFilterUpdate: PropTypes.func.isRequired,
-    onReload: PropTypes.func.isRequired
-  };
-
-  handleStatusChange = (status /*: string */) => {
-    this.props.onFilterUpdate({ status });
-  };
-
-  handleTypeChange = (taskType /*: string */) => {
-    this.props.onFilterUpdate({ taskType });
-  };
-
-  handleCurrentsChange = (currents /*: string */) => {
-    this.props.onFilterUpdate({ currents });
-  };
-
-  handleDateChange = (date /*: { maxExecutedAt?: Date; minSubmittedAt?: Date } */) => {
-    this.props.onFilterUpdate(date);
-  };
-
-  handleQueryChange = (query /*: string */) => {
-    this.props.onFilterUpdate({ query });
-  };
-
-  handleReset = () => {
-    this.props.onFilterUpdate(DEFAULT_FILTERS);
-  };
-
-  renderSearchBox() {
-    const { component, query } = this.props;
-
-    if (component) {
-      // do not render search form on the project-level page
-      return null;
-    }
-
-    return (
-      <li className="bt-search-form-large">
-        <SearchBox
-          onChange={this.handleQueryChange}
-          placeholder={translate('background_tasks.search_by_task_or_component')}
-          value={query}
-        />
-      </li>
-    );
-  }
-
-  render() {
-    const {
-      loading,
-      component,
-      types,
-      status,
-      taskType,
-      currents,
-      minSubmittedAt,
-      maxExecutedAt
-    } = this.props;
-
-    return (
-      <section className="big-spacer-top big-spacer-bottom">
-        <ul className="bt-search-form">
-          <li>
-            <h6 className="bt-search-form-label">{translate('status')}</h6>
-            <StatusFilter onChange={this.handleStatusChange} value={status} />
-          </li>
-          {types.length > 1 && (
-            <li>
-              <h6 className="bt-search-form-label">{translate('type')}</h6>
-              <TypesFilter onChange={this.handleTypeChange} types={types} value={taskType} />
-            </li>
-          )}
-          {!component && (
-            <li>
-              <h6 className="bt-search-form-label">
-                {translate('background_tasks.currents_filter.ONLY_CURRENTS')}
-              </h6>
-              <CurrentsFilter onChange={this.handleCurrentsChange} value={currents} />
-            </li>
-          )}
-          <li>
-            <h6 className="bt-search-form-label">{translate('date')}</h6>
-            <DateFilter
-              maxExecutedAt={maxExecutedAt}
-              minSubmittedAt={minSubmittedAt}
-              onChange={this.handleDateChange}
-            />
-          </li>
-
-          {this.renderSearchBox()}
-
-          <li className="nowrap">
-            <Button className="js-reload" disabled={loading} onClick={this.props.onReload}>
-              {translate('reload')}
-            </Button>{' '}
-            <Button disabled={loading} onClick={this.handleReset}>
-              {translate('reset_verb')}
-            </Button>
-          </li>
-        </ul>
-      </section>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx
new file mode 100644 (file)
index 0000000..e7b2687
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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 StatusFilter from './StatusFilter';
+import TypesFilter from './TypesFilter';
+import CurrentsFilter from './CurrentsFilter';
+import DateFilter from './DateFilter';
+import { DEFAULT_FILTERS } from '../constants';
+import SearchBox from '../../../components/controls/SearchBox';
+import { Button } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
+import { Query } from '../utils';
+
+interface Props {
+  component?: unknown;
+  currents: string;
+  loading: boolean;
+  onFilterUpdate: (changes: Partial<Query>) => void;
+  onReload: () => void;
+  query: string;
+  status: string;
+  taskType: string;
+  maxExecutedAt: Date | undefined;
+  minSubmittedAt: Date | undefined;
+  types: string[];
+}
+
+export default class Search extends React.PureComponent<Props> {
+  handleStatusChange = (status: string) => {
+    this.props.onFilterUpdate({ status });
+  };
+
+  handleTypeChange = (taskType: string) => {
+    this.props.onFilterUpdate({ taskType });
+  };
+
+  handleCurrentsChange = (currents: string) => {
+    this.props.onFilterUpdate({ currents });
+  };
+
+  handleDateChange = (date: { maxExecutedAt?: Date; minSubmittedAt?: Date }) => {
+    this.props.onFilterUpdate(date);
+  };
+
+  handleQueryChange = (query: string) => {
+    this.props.onFilterUpdate({ query });
+  };
+
+  handleReset = () => {
+    this.props.onFilterUpdate(DEFAULT_FILTERS);
+  };
+
+  renderSearchBox() {
+    const { component, query } = this.props;
+
+    if (component) {
+      // do not render search form on the project-level page
+      return null;
+    }
+
+    return (
+      <li className="bt-search-form-large">
+        <SearchBox
+          onChange={this.handleQueryChange}
+          placeholder={translate('background_tasks.search_by_task_or_component')}
+          value={query}
+        />
+      </li>
+    );
+  }
+
+  render() {
+    const {
+      loading,
+      component,
+      types,
+      status,
+      taskType,
+      currents,
+      minSubmittedAt,
+      maxExecutedAt
+    } = this.props;
+
+    return (
+      <section className="big-spacer-top big-spacer-bottom">
+        <ul className="bt-search-form">
+          <li>
+            <h6 className="bt-search-form-label">{translate('status')}</h6>
+            <StatusFilter onChange={this.handleStatusChange} value={status} />
+          </li>
+          {types.length > 1 && (
+            <li>
+              <h6 className="bt-search-form-label">{translate('type')}</h6>
+              <TypesFilter onChange={this.handleTypeChange} types={types} value={taskType} />
+            </li>
+          )}
+          {!component && (
+            <li>
+              <h6 className="bt-search-form-label">
+                {translate('background_tasks.currents_filter.ONLY_CURRENTS')}
+              </h6>
+              <CurrentsFilter onChange={this.handleCurrentsChange} value={currents} />
+            </li>
+          )}
+          <li>
+            <h6 className="bt-search-form-label">{translate('date')}</h6>
+            <DateFilter
+              maxExecutedAt={maxExecutedAt}
+              minSubmittedAt={minSubmittedAt}
+              onChange={this.handleDateChange}
+            />
+          </li>
+
+          {this.renderSearchBox()}
+
+          <li className="nowrap">
+            <Button className="js-reload" disabled={loading} onClick={this.props.onReload}>
+              {translate('reload')}
+            </Button>{' '}
+            <Button disabled={loading} onClick={this.handleReset}>
+              {translate('reset_verb')}
+            </Button>
+          </li>
+        </ul>
+      </section>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js
deleted file mode 100644 (file)
index 5e47a33..0000000
+++ /dev/null
@@ -1,126 +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.
- */
-/* @flow */
-import React from 'react';
-import Tooltip from '../../../components/controls/Tooltip';
-import { DeleteButton } from '../../../components/ui/buttons';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = {
-  failingCount: number,
-  isSystemAdmin?: boolean,
-  pendingCount: number,
-  onShowFailing: () => void,
-  onCancelAllPending: () => void
-};
-*/
-
-/*::
-type State = Object;
-*/
-
-export default class Stats extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  handleShowFailing = (event /*: Object */) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.props.onShowFailing();
-  };
-
-  renderPending() {
-    if (this.props.pendingCount == null) {
-      return null;
-    }
-    if (this.props.pendingCount > 0) {
-      return (
-        <span>
-          <span className="js-pending-count emphasised-measure">{this.props.pendingCount}</span>
-          &nbsp;
-          {translate('background_tasks.pending')}
-          {this.props.isSystemAdmin && (
-            <Tooltip overlay={translate('background_tasks.cancel_all_tasks')}>
-              <DeleteButton
-                className="js-cancel-pending spacer-left"
-                onClick={this.props.onCancelAllPending}
-              />
-            </Tooltip>
-          )}
-        </span>
-      );
-    } else {
-      return (
-        <span>
-          <span className="js-pending-count emphasised-measure">{this.props.pendingCount}</span>
-          &nbsp;
-          {translate('background_tasks.pending')}
-        </span>
-      );
-    }
-  }
-
-  renderFailures() {
-    if (this.props.failingCount == null) {
-      return null;
-    }
-
-    if (this.props.component) {
-      return null;
-    }
-
-    if (this.props.failingCount > 0) {
-      return (
-        <span>
-          <Tooltip overlay={translate('background_tasks.failing_count')}>
-            <a
-              className="js-failures-count emphasised-measure"
-              href="#"
-              onClick={this.handleShowFailing}>
-              {this.props.failingCount}
-            </a>
-          </Tooltip>
-          &nbsp;
-          {translate('background_tasks.failures')}
-        </span>
-      );
-    } else {
-      return (
-        <span>
-          <Tooltip overlay={translate('background_tasks.failing_count')}>
-            <span className="js-failures-count emphasised-measure">{this.props.failingCount}</span>
-          </Tooltip>
-          &nbsp;
-          {translate('background_tasks.failures')}
-        </span>
-      );
-    }
-  }
-
-  render() {
-    return (
-      <section className="big-spacer-top big-spacer-bottom">
-        <span>{this.renderPending()}</span>
-        <span className="huge-spacer-left">{this.renderFailures()}</span>
-      </section>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx
new file mode 100644 (file)
index 0000000..31d9365
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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 Tooltip from '../../../components/controls/Tooltip';
+import { DeleteButton } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  component?: unknown;
+  failingCount?: number;
+  isSystemAdmin?: boolean;
+  pendingCount?: number;
+  onShowFailing: () => void;
+  onCancelAllPending: () => void;
+}
+
+export default class Stats extends React.PureComponent<Props> {
+  handleShowFailing = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onShowFailing();
+  };
+
+  renderPending() {
+    if (this.props.pendingCount === undefined) {
+      return null;
+    }
+    if (this.props.pendingCount > 0) {
+      return (
+        <span>
+          <span className="js-pending-count emphasised-measure">{this.props.pendingCount}</span>
+          &nbsp;
+          {translate('background_tasks.pending')}
+          {this.props.isSystemAdmin && (
+            <Tooltip overlay={translate('background_tasks.cancel_all_tasks')}>
+              <DeleteButton
+                className="js-cancel-pending spacer-left"
+                onClick={this.props.onCancelAllPending}
+              />
+            </Tooltip>
+          )}
+        </span>
+      );
+    } else {
+      return (
+        <span>
+          <span className="js-pending-count emphasised-measure">{this.props.pendingCount}</span>
+          &nbsp;
+          {translate('background_tasks.pending')}
+        </span>
+      );
+    }
+  }
+
+  renderFailures() {
+    if (this.props.failingCount === undefined) {
+      return null;
+    }
+
+    if (this.props.component) {
+      return null;
+    }
+
+    if (this.props.failingCount > 0) {
+      return (
+        <span>
+          <Tooltip overlay={translate('background_tasks.failing_count')}>
+            <a
+              className="js-failures-count emphasised-measure"
+              href="#"
+              onClick={this.handleShowFailing}>
+              {this.props.failingCount}
+            </a>
+          </Tooltip>
+          &nbsp;
+          {translate('background_tasks.failures')}
+        </span>
+      );
+    } else {
+      return (
+        <span>
+          <Tooltip overlay={translate('background_tasks.failing_count')}>
+            <span className="js-failures-count emphasised-measure">{this.props.failingCount}</span>
+          </Tooltip>
+          &nbsp;
+          {translate('background_tasks.failures')}
+        </span>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <section className="big-spacer-top big-spacer-bottom">
+        <span>{this.renderPending()}</span>
+        <span className="huge-spacer-left">{this.renderFailures()}</span>
+      </section>
+    );
+  }
+}
index f4ab2473ec5295dd891b43be1049d53173d17878..2cc54c8f6de80c727964bf793e28ee8ae2f454fc 100644 (file)
@@ -25,4 +25,4 @@ const mapStateToProps = (state: Store) => ({
   isSystemAdmin: !!getAppState(state).canAdmin
 });
 
-export default connect(mapStateToProps)(Stats as any);
+export default connect(mapStateToProps)(Stats);
index c51a705688bda80ac213fd64dd1997a373d18ed7..7dd532d3f98626d5a8cadbfa5c345fbd6d97ecfe 100644 (file)
@@ -29,7 +29,7 @@ import TaskSubmitter from './TaskSubmitter';
 import { Task as TaskType } from '../../../app/types';
 
 interface Props {
-  component?: {};
+  component?: unknown;
   onCancelTask: (task: TaskType) => void;
   onFilterTask: (task: TaskType) => void;
   task: TaskType;
index 119d8ac7d61f66cb56be431eb8fc37dc149bf812..814611067c7e3ff2db949c70720fb25b34491799 100644 (file)
@@ -32,7 +32,7 @@ const AnalysisWarningsModal = lazyLoad(
 );
 
 interface Props {
-  component?: {};
+  component?: unknown;
   onCancelTask: (task: Task) => void;
   onFilterTask: (task: Task) => void;
   task: Task;
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.js
deleted file mode 100644 (file)
index 3d5f6bb..0000000
+++ /dev/null
@@ -1,85 +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.
- */
-/* @flow */
-import React from 'react';
-import classNames from 'classnames';
-import Task from './Task';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = {
-  tasks: Array<*>,
-  component: Object,
-  loading: boolean,
-  onCancelTask: Function,
-  onFilterTask: Function
-};
-*/
-
-/*::
-type State = Object;
-*/
-
-export default class Tasks extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  render() {
-    const { tasks, component, loading, onCancelTask, onFilterTask } = this.props;
-
-    const className = classNames('data zebra zebra-hover background-tasks', {
-      'new-loading': loading
-    });
-
-    return (
-      <div className="boxed-group boxed-group-inner">
-        <table className={className}>
-          <thead>
-            <tr>
-              <th>{translate('background_tasks.table.status')}</th>
-              <th>{translate('background_tasks.table.task')}</th>
-              <th>{translate('background_tasks.table.id')}</th>
-              <th>{translate('background_tasks.table.submitter')}</th>
-              <th>&nbsp;</th>
-              <th className="text-right">{translate('background_tasks.table.submitted')}</th>
-              <th className="text-right">{translate('background_tasks.table.started')}</th>
-              <th className="text-right">{translate('background_tasks.table.finished')}</th>
-              <th className="text-right">{translate('background_tasks.table.duration')}</th>
-              <th>&nbsp;</th>
-            </tr>
-          </thead>
-          <tbody>
-            {tasks.map((task, index, tasks) => (
-              <Task
-                component={component}
-                key={task.id}
-                onCancelTask={onCancelTask}
-                onFilterTask={onFilterTask}
-                previousTask={index > 0 ? tasks[index - 1] : undefined}
-                task={task}
-                tasks={tasks}
-              />
-            ))}
-          </tbody>
-        </table>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx
new file mode 100644 (file)
index 0000000..db104a8
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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 Task from './Task';
+import { Task as TaskType } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  tasks: TaskType[];
+  component?: unknown;
+  loading: boolean;
+  onCancelTask: (task: TaskType) => void;
+  onFilterTask: (task: TaskType) => void;
+}
+
+export default function Tasks({ tasks, component, loading, onCancelTask, onFilterTask }: Props) {
+  const className = classNames('data zebra zebra-hover background-tasks', {
+    'new-loading': loading
+  });
+
+  return (
+    <div className="boxed-group boxed-group-inner">
+      <table className={className}>
+        <thead>
+          <tr>
+            <th>{translate('background_tasks.table.status')}</th>
+            <th>{translate('background_tasks.table.task')}</th>
+            <th>{translate('background_tasks.table.id')}</th>
+            <th>{translate('background_tasks.table.submitter')}</th>
+            <th>&nbsp;</th>
+            <th className="text-right">{translate('background_tasks.table.submitted')}</th>
+            <th className="text-right">{translate('background_tasks.table.started')}</th>
+            <th className="text-right">{translate('background_tasks.table.finished')}</th>
+            <th className="text-right">{translate('background_tasks.table.duration')}</th>
+            <th>&nbsp;</th>
+          </tr>
+        </thead>
+        <tbody>
+          {tasks.map((task, index, tasks) => (
+            <Task
+              component={component}
+              key={task.id}
+              onCancelTask={onCancelTask}
+              onFilterTask={onFilterTask}
+              previousTask={index > 0 ? tasks[index - 1] : undefined}
+              task={task}
+            />
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/constants.js b/server/sonar-web/src/main/js/apps/background-tasks/constants.js
deleted file mode 100644 (file)
index 5cee6f2..0000000
+++ /dev/null
@@ -1,55 +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.
- */
-/* @flow */
-export const STATUSES = {
-  ALL: '__ALL__',
-  ALL_EXCEPT_PENDING: '__ALL_EXCEPT_PENDING__',
-  PENDING: 'PENDING',
-  IN_PROGRESS: 'IN_PROGRESS',
-  SUCCESS: 'SUCCESS',
-  FAILED: 'FAILED',
-  CANCELED: 'CANCELED'
-};
-
-export const ALL_TYPES = 'ALL_TYPES';
-
-export const CURRENTS = {
-  ALL: '__ALL__',
-  ONLY_CURRENTS: 'CURRENTS'
-};
-
-export const DATE = {
-  ANY: 'ANY',
-  TODAY: 'TODAY',
-  CUSTOM: 'CUSTOM'
-};
-
-export const DEFAULT_FILTERS = {
-  status: STATUSES.ALL_EXCEPT_PENDING,
-  taskType: ALL_TYPES,
-  currents: CURRENTS.ALL,
-  minSubmittedAt: undefined,
-  maxExecutedAt: undefined,
-  query: ''
-};
-
-export const DATE_FORMAT = 'YYYY-MM-DD';
-
-export const DEBOUNCE_DELAY = 250;
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/constants.ts b/server/sonar-web/src/main/js/apps/background-tasks/constants.ts
new file mode 100644 (file)
index 0000000..df6bcf7
--- /dev/null
@@ -0,0 +1,56 @@
+import { Query } from './utils';
+
+/*
+ * 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.
+ */
+export const STATUSES = {
+  ALL: '__ALL__',
+  ALL_EXCEPT_PENDING: '__ALL_EXCEPT_PENDING__',
+  PENDING: 'PENDING',
+  IN_PROGRESS: 'IN_PROGRESS',
+  SUCCESS: 'SUCCESS',
+  FAILED: 'FAILED',
+  CANCELED: 'CANCELED'
+};
+
+export const ALL_TYPES = 'ALL_TYPES';
+
+export const CURRENTS = {
+  ALL: '__ALL__',
+  ONLY_CURRENTS: 'CURRENTS'
+};
+
+export const DATE = {
+  ANY: 'ANY',
+  TODAY: 'TODAY',
+  CUSTOM: 'CUSTOM'
+};
+
+export const DEFAULT_FILTERS: Query = {
+  status: STATUSES.ALL_EXCEPT_PENDING,
+  taskType: ALL_TYPES,
+  currents: CURRENTS.ALL,
+  minSubmittedAt: undefined,
+  maxExecutedAt: undefined,
+  query: ''
+};
+
+export const DATE_FORMAT = 'YYYY-MM-DD';
+
+export const DEBOUNCE_DELAY = 250;
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/utils.js b/server/sonar-web/src/main/js/apps/background-tasks/utils.js
deleted file mode 100644 (file)
index a023e71..0000000
+++ /dev/null
@@ -1,105 +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 { STATUSES, ALL_TYPES, CURRENTS } from './constants';
-import { toShortNotSoISOString } from '../../helpers/dates';
-
-export function updateTask(tasks, newTask) {
-  return tasks.map(task => (task.id === newTask.id ? newTask : task));
-}
-
-export function mapFiltersToParameters(filters /*: Object */ = {}) {
-  const parameters = {};
-
-  if (filters.status === STATUSES.ALL) {
-    parameters.status = [
-      STATUSES.PENDING,
-      STATUSES.IN_PROGRESS,
-      STATUSES.SUCCESS,
-      STATUSES.FAILED,
-      STATUSES.CANCELED
-    ].join();
-  } else if (filters.status === STATUSES.ALL_EXCEPT_PENDING) {
-    parameters.status = [
-      STATUSES.IN_PROGRESS,
-      STATUSES.SUCCESS,
-      STATUSES.FAILED,
-      STATUSES.CANCELED
-    ].join();
-  } else {
-    parameters.status = filters.status;
-  }
-
-  if (filters.taskType !== ALL_TYPES) {
-    parameters.type = filters.taskType;
-  }
-
-  if (filters.currents !== CURRENTS.ALL) {
-    parameters.onlyCurrents = true;
-  }
-
-  if (filters.minSubmittedAt) {
-    parameters.minSubmittedAt = toShortNotSoISOString(filters.minSubmittedAt);
-  }
-
-  if (filters.maxExecutedAt) {
-    parameters.maxExecutedAt = toShortNotSoISOString(filters.maxExecutedAt);
-  }
-
-  if (filters.query) {
-    parameters.componentQuery = filters.query;
-  }
-
-  if (filters.lastPage !== 1) {
-    parameters.p = filters.lastPage;
-  }
-
-  return parameters;
-}
-
-const ONE_SECOND = 1000;
-const ONE_MINUTE = 60 * ONE_SECOND;
-const ONE_HOUR = 60 * ONE_MINUTE;
-
-function format(int, suffix) {
-  return `${int}${suffix}`;
-}
-
-export function formatDuration(value /*: ?number */) {
-  if (!value) {
-    return '';
-  }
-  if (value < ONE_SECOND) {
-    return format(value, 'ms');
-  } else if (value < ONE_SECOND * 10) {
-    const seconds = Math.floor(value / ONE_SECOND);
-    const ms = value - seconds * ONE_SECOND;
-    return seconds + '.' + format(ms, 's');
-  } else if (value < ONE_MINUTE) {
-    const seconds = Math.floor(value / ONE_SECOND);
-    return format(seconds, 's');
-  } else if (value < ONE_MINUTE * 10) {
-    const minutes = Math.floor(value / ONE_MINUTE);
-    const seconds = Math.floor((value - minutes * ONE_MINUTE) / ONE_SECOND);
-    return format(minutes, 'min') + ' ' + format(seconds, 's');
-  }
-  const hours = Math.floor(value / ONE_HOUR);
-  const minutes = Math.floor((value - hours * ONE_HOUR) / ONE_MINUTE);
-  return format(hours, 'h') + ' ' + format(minutes, 'min');
-}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/utils.ts b/server/sonar-web/src/main/js/apps/background-tasks/utils.ts
new file mode 100644 (file)
index 0000000..848e240
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * 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 { STATUSES, CURRENTS, ALL_TYPES } from './constants';
+import { toShortNotSoISOString } from '../../helpers/dates';
+import { Task } from '../../app/types';
+
+export interface Query {
+  currents: string;
+  maxExecutedAt?: Date;
+  minSubmittedAt?: Date;
+  query: string;
+  status: string;
+  taskType: string;
+}
+
+export function updateTask(tasks: Task[], newTask: Task) {
+  return tasks.map(task => (task.id === newTask.id ? newTask : task));
+}
+
+export function mapFiltersToParameters(filters: Partial<Query> = {}) {
+  const parameters: any = {};
+
+  if (filters.status === STATUSES.ALL) {
+    parameters.status = [
+      STATUSES.PENDING,
+      STATUSES.IN_PROGRESS,
+      STATUSES.SUCCESS,
+      STATUSES.FAILED,
+      STATUSES.CANCELED
+    ].join();
+  } else if (filters.status === STATUSES.ALL_EXCEPT_PENDING) {
+    parameters.status = [
+      STATUSES.IN_PROGRESS,
+      STATUSES.SUCCESS,
+      STATUSES.FAILED,
+      STATUSES.CANCELED
+    ].join();
+  } else {
+    parameters.status = filters.status;
+  }
+
+  if (filters.taskType !== ALL_TYPES) {
+    parameters.type = filters.taskType;
+  }
+
+  if (filters.currents !== CURRENTS.ALL) {
+    parameters.onlyCurrents = true;
+  }
+
+  if (filters.minSubmittedAt) {
+    parameters.minSubmittedAt = toShortNotSoISOString(filters.minSubmittedAt);
+  }
+
+  if (filters.maxExecutedAt) {
+    parameters.maxExecutedAt = toShortNotSoISOString(filters.maxExecutedAt);
+  }
+
+  if (filters.query) {
+    parameters.componentQuery = filters.query;
+  }
+
+  return parameters;
+}
+
+const ONE_SECOND = 1000;
+const ONE_MINUTE = 60 * ONE_SECOND;
+const ONE_HOUR = 60 * ONE_MINUTE;
+
+function format(int: number, suffix: string) {
+  return `${int}${suffix}`;
+}
+
+export function formatDuration(value: number | undefined) {
+  if (!value) {
+    return '';
+  }
+  if (value < ONE_SECOND) {
+    return format(value, 'ms');
+  } else if (value < ONE_SECOND * 10) {
+    const seconds = Math.floor(value / ONE_SECOND);
+    const ms = value - seconds * ONE_SECOND;
+    return seconds + '.' + format(ms, 's');
+  } else if (value < ONE_MINUTE) {
+    const seconds = Math.floor(value / ONE_SECOND);
+    return format(seconds, 's');
+  } else if (value < ONE_MINUTE * 10) {
+    const minutes = Math.floor(value / ONE_MINUTE);
+    const seconds = Math.floor((value - minutes * ONE_MINUTE) / ONE_SECOND);
+    return format(minutes, 'min') + ' ' + format(seconds, 's');
+  }
+  const hours = Math.floor(value / ONE_HOUR);
+  const minutes = Math.floor((value - hours * ONE_HOUR) / ONE_MINUTE);
+  return format(hours, 'h') + ' ' + format(minutes, 'min');
+}