Browse Source

rewrite the rest of the background tasks app in ts

tags/7.5
Stas Vilchik 5 years ago
parent
commit
a8fdcdc653

+ 5
- 3
server/sonar-web/src/main/js/api/ce.ts View 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);
}


server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js → server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-test.tsx View File

@@ -17,13 +17,13 @@
* 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 * 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 { change, click } from '../../../helpers/testUtils';
import { click } from '../../../helpers/testUtils';

const stub = jest.fn();

@@ -38,12 +38,14 @@ describe('Constants', () => {
});

describe('Search', () => {
const defaultProps = {
const defaultProps: Search['props'] = {
...DEFAULT_FILTERS,
loading: false,
types: [],
onFilterUpdate: () => true,
onReload: () => true
onReload: () => true,
maxExecutedAt: undefined,
minSubmittedAt: undefined
};

it('should render search form', () => {
@@ -60,7 +62,7 @@ describe('Search', () => {
const searchSpy = jest.fn();
const component = shallow(<Search {...defaultProps} onFilterUpdate={searchSpy} />);
const searchInput = component.find('SearchBox');
searchInput.prop('onChange')('some search query');
searchInput.prop<Function>('onChange')('some search query');
setTimeout(() => {
expect(searchSpy).toBeCalledWith({ query: 'some search query' });
done();

server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js → server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx View File

@@ -17,12 +17,12 @@
* 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 * 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';
@@ -37,67 +37,59 @@ import {
cancelAllTasks,
cancelTask as cancelTaskAPI
} from '../../../api/ce';
import { updateTask, mapFiltersToParameters } from '../utils';
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';

/*::
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: '',
interface Props {
component?: { id: string };
fetchOrganizations: (keys: string[]) => void;
location: Location;
router: Pick<InjectedRouter, 'push'>;
}

// stats
pendingCount: 0,
failingCount: 0
};
interface State {
loading: boolean;
tasks: Task[];
types?: string[];
query: string;
pendingCount: number;
failingCount: number;
}

componentWillMount() {
this.loadTasksDebounced = debounce(this.loadTasks.bind(this), DEBOUNCE_DELAY);
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();
});
getTypes().then(
types => {
this.setState({ types });
this.loadTasks();
},
() => {}
);
}

componentDidUpdate(prevProps /*: Props */) {
componentDidUpdate(prevProps: Props) {
if (
prevProps.component !== this.props.component ||
prevProps.location !== this.props.location
@@ -110,7 +102,13 @@ class BackgroundTasksApp extends React.PureComponent {
this.mounted = false;
}

loadTasks() {
stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

loadTasks = () => {
this.setState({ loading: true });

const status = this.props.location.query.status || DEFAULT_FILTERS.status;
@@ -131,7 +129,7 @@ class BackgroundTasksApp extends React.PureComponent {
Promise.all([getActivity(parameters), getStatus(parameters.componentId)]).then(responses => {
if (this.mounted) {
const [activity, status] = responses;
const tasks = activity.tasks;
const { tasks } = activity;

const pendingCount = status.pending;
const failingCount = status.failing;
@@ -146,14 +144,14 @@ class BackgroundTasksApp extends React.PureComponent {
loading: false
});
}
});
}
}, this.stopLoading);
};

handleFilterUpdate(nextState /*: Object */) {
handleFilterUpdate = (nextState: Partial<Query>) => {
const nextQuery = { ...this.props.location.query, ...nextState };

// remove defaults
Object.keys(DEFAULT_FILTERS).forEach(key => {
Object.keys(DEFAULT_FILTERS).forEach((key: keyof typeof DEFAULT_FILTERS) => {
if (nextQuery[key] === DEFAULT_FILTERS[key]) {
delete nextQuery[key];
}
@@ -167,26 +165,28 @@ class BackgroundTasksApp extends React.PureComponent {
nextQuery.maxExecutedAt = toShortNotSoISOString(nextQuery.maxExecutedAt);
}

this.context.router.push({
this.props.router.push({
pathname: this.props.location.pathname,
query: nextQuery
});
}
};

handleCancelTask(task) {
handleCancelTask = (task: 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 });
this.setState(state => ({
tasks: updateTask(state.tasks, nextTask),
loading: false
}));
}
});
}
}, this.stopLoading);
};

handleFilterTask(task) {
handleFilterTask = (task: Task) => {
this.handleFilterUpdate({ query: task.componentKey });
}
};

handleShowFailing() {
this.handleFilterUpdate({
@@ -203,7 +203,7 @@ class BackgroundTasksApp extends React.PureComponent {
if (this.mounted) {
this.loadTasks();
}
});
}, this.stopLoading);
}

render() {
@@ -234,8 +234,8 @@ class BackgroundTasksApp extends React.PureComponent {
<StatsContainer
component={component}
failingCount={failingCount}
onCancelAllPending={this.handleCancelAllPending.bind(this)}
onShowFailing={this.handleShowFailing.bind(this)}
onCancelAllPending={this.handleCancelAllPending}
onShowFailing={this.handleShowFailing}
pendingCount={pendingCount}
/>

@@ -245,7 +245,7 @@ class BackgroundTasksApp extends React.PureComponent {
loading={loading}
maxExecutedAt={maxExecutedAt}
minSubmittedAt={minSubmittedAt}
onFilterUpdate={this.handleFilterUpdate.bind(this)}
onFilterUpdate={this.handleFilterUpdate}
onReload={this.loadTasksDebounced}
query={query}
status={status}
@@ -256,8 +256,8 @@ class BackgroundTasksApp extends React.PureComponent {
<Tasks
component={component}
loading={loading}
onCancelTask={this.handleCancelTask.bind(this)}
onFilterTask={this.handleFilterTask.bind(this)}
onCancelTask={this.handleCancelTask}
onFilterTask={this.handleFilterTask}
tasks={tasks}
/>


server/sonar-web/src/main/js/apps/background-tasks/components/Search.js → server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx View File

@@ -17,9 +17,7 @@
* 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 * as React from 'react';
import StatusFilter from './StatusFilter';
import TypesFilter from './TypesFilter';
import CurrentsFilter from './CurrentsFilter';
@@ -28,35 +26,40 @@ 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';

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
};
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[];
}

handleStatusChange = (status /*: string */) => {
export default class Search extends React.PureComponent<Props> {
handleStatusChange = (status: string) => {
this.props.onFilterUpdate({ status });
};

handleTypeChange = (taskType /*: string */) => {
handleTypeChange = (taskType: string) => {
this.props.onFilterUpdate({ taskType });
};

handleCurrentsChange = (currents /*: string */) => {
handleCurrentsChange = (currents: string) => {
this.props.onFilterUpdate({ currents });
};

handleDateChange = (date /*: { maxExecutedAt?: Date; minSubmittedAt?: Date } */) => {
handleDateChange = (date: { maxExecutedAt?: Date; minSubmittedAt?: Date }) => {
this.props.onFilterUpdate(date);
};

handleQueryChange = (query /*: string */) => {
handleQueryChange = (query: string) => {
this.props.onFilterUpdate({ query });
};


server/sonar-web/src/main/js/apps/background-tasks/components/Stats.js → server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx View File

@@ -17,38 +17,29 @@
* 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 * as 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; */
interface Props {
component?: unknown;
failingCount?: number;
isSystemAdmin?: boolean;
pendingCount?: number;
onShowFailing: () => void;
onCancelAllPending: () => void;
}

handleShowFailing = (event /*: Object */) => {
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 == null) {
if (this.props.pendingCount === undefined) {
return null;
}
if (this.props.pendingCount > 0) {
@@ -79,7 +70,7 @@ export default class Stats extends React.PureComponent {
}

renderFailures() {
if (this.props.failingCount == null) {
if (this.props.failingCount === undefined) {
return null;
}


+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/StatsContainer.tsx View 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);

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/Task.tsx View 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;

+ 1
- 1
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx View File

@@ -32,7 +32,7 @@ const AnalysisWarningsModal = lazyLoad(
);

interface Props {
component?: {};
component?: unknown;
onCancelTask: (task: Task) => void;
onFilterTask: (task: Task) => void;
task: Task;

+ 0
- 85
server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.js View File

@@ -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>
);
}
}

+ 71
- 0
server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx View File

@@ -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>
);
}

server/sonar-web/src/main/js/apps/background-tasks/constants.js → server/sonar-web/src/main/js/apps/background-tasks/constants.ts View File

@@ -1,3 +1,5 @@
import { Query } from './utils';

/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
@@ -17,7 +19,6 @@
* 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__',
@@ -41,7 +42,7 @@ export const DATE = {
CUSTOM: 'CUSTOM'
};

export const DEFAULT_FILTERS = {
export const DEFAULT_FILTERS: Query = {
status: STATUSES.ALL_EXCEPT_PENDING,
taskType: ALL_TYPES,
currents: CURRENTS.ALL,

server/sonar-web/src/main/js/apps/background-tasks/utils.js → server/sonar-web/src/main/js/apps/background-tasks/utils.ts View File

@@ -17,15 +17,25 @@
* 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 { STATUSES, CURRENTS, ALL_TYPES } from './constants';
import { toShortNotSoISOString } from '../../helpers/dates';
import { Task } from '../../app/types';

export function updateTask(tasks, newTask) {
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 /*: Object */ = {}) {
const parameters = {};
export function mapFiltersToParameters(filters: Partial<Query> = {}) {
const parameters: any = {};

if (filters.status === STATUSES.ALL) {
parameters.status = [
@@ -66,10 +76,6 @@ export function mapFiltersToParameters(filters /*: Object */ = {}) {
parameters.componentQuery = filters.query;
}

if (filters.lastPage !== 1) {
parameters.p = filters.lastPage;
}

return parameters;
}

@@ -77,11 +83,11 @@ const ONE_SECOND = 1000;
const ONE_MINUTE = 60 * ONE_SECOND;
const ONE_HOUR = 60 * ONE_MINUTE;

function format(int, suffix) {
function format(int: number, suffix: string) {
return `${int}${suffix}`;
}

export function formatDuration(value /*: ?number */) {
export function formatDuration(value: number | undefined) {
if (!value) {
return '';
}

Loading…
Cancel
Save