"curly": 2, // specify curly brace conventions for all control statements
"default-case": 1, // require default case in switch statements (off by default)
"dot-notation": 0, // encourages use of dot notation whenever possible
- "eqeqeq": 2, // require the use of === and !==
+ "eqeqeq": [2, "allow-null"],// require the use of === and !==
"guard-for-in": 2, // make sure for-in loops have an if statement (off by default)
"no-alert": 1, // disallow the use of alert, confirm, and prompt
"no-caller": 2, // disallow use of arguments.caller or arguments.callee
'jquery': 'libs/third-party/shim/jquery-shim'
'backbone': 'libs/third-party/shim/backbone-shim'
'backbone.marionette': 'libs/third-party/shim/marionette-shim'
+ 'moment': 'libs/third-party/shim/moment-shim'
issuesContext: options:
name: 'apps/issues/app-context'
"jquery": "2.1.4",
"jsdom": "^6.5.1",
"mocha": "^2.3.3",
+ "moment": "^2.10.6",
"react": "0.13.3",
"sinon": "^1.15.4",
"sinon-chai": "^2.8.0",
export default {
start (options) {
- var el = document.querySelector(options.el);
- React.render(<Main options={options}/>, el);
+ window.requestMessages().done(() => {
+ let el = document.querySelector(options.el);
+ React.render(<Main options={options}/>, el);
+ });
}
};
render() {
return (
<header className="page-header">
- <h1 className="page-title">Background Tasks</h1>
- <p className="page-description">The server is in charge to process reports submitted by batch analyses. This
- page allows to monitor the queue of pending reports to process, and gives access to the history of past
- analyses.</p>
+ <h1 className="page-title">{window.t('background_tasks.page')}</h1>
+ <p className="page-description">{window.t('background_tasks.page.description')}</p>
</header>
-
);
}
});
--- /dev/null
+const ONE_SECOND = 1000;
+const ONE_MINUTE = 60 * ONE_SECOND;
+
+function format(int, suffix) {
+ return `${int}${suffix}`;
+}
+
+export function formatDuration(value) {
+ if (!value) {
+ return '';
+ }
+ if (value >= ONE_MINUTE) {
+ let minutes = Math.round(value / ONE_MINUTE);
+ return format(minutes, 'min');
+ } else if (value >= ONE_SECOND) {
+ let seconds = Math.round(value / ONE_SECOND);
+ return format(seconds, 's');
+ } else {
+ return format(value, 'ms');
+ }
+}
},
onTaskCanceled(task) {
- cancelTask(task.id).done(data => {
- _.extend(task, data.task);
- this.forceUpdate();
- });
+ cancelTask(task.id).then(this.requestData);
},
cancelPending() {
<Stats {...this.state} cancelPending={this.cancelPending} showFailures={this.showFailures}/>
<Search {...this.props} {...this.state}
+ refresh={this.requestData}
onStatusChange={this.onStatusChange}
onCurrentsChange={this.onCurrentsChange}
onDateChange={this.onDateChange}
getCurrentsOptions() {
return [
- { value: CURRENTS.ALL, label: 'All' },
- { value: CURRENTS.ONLY_CURRENTS, label: 'Only Currents' }
+ { value: CURRENTS.ALL, label: window.t('background_tasks.currents_filter.ALL') },
+ { value: CURRENTS.ONLY_CURRENTS, label: window.t('background_tasks.currents_filter.ONLY_CURRENTS') }
];
},
getStatusOptions() {
return [
- { value: STATUSES.ALL, label: 'All' },
- { value: STATUSES.SUCCESS, label: 'Success' },
- { value: STATUSES.FAILED, label: 'Failed' },
- { value: STATUSES.CANCELED, label: 'Canceled' }
+ { value: STATUSES.ALL, label: window.t('background_task.status.ALL') },
+ { value: STATUSES.SUCCESS, label: window.t('background_task.status.SUCCESS') },
+ { value: STATUSES.FAILED, label: window.t('background_task.status.FAILED') },
+ { value: STATUSES.CANCELED, label: window.t('background_task.status.CANCELED') }
];
},
getDateOptions() {
return [
- { value: DATE.ANY, label: 'Any Date' },
- { value: DATE.TODAY, label: 'Today' },
- { value: DATE.CUSTOM, label: 'Custom' }
+ { value: DATE.ANY, label: window.t('background_tasks.date_filter.ALL') },
+ { value: DATE.TODAY, label: window.t('background_tasks.date_filter.TODAY') },
+ { value: DATE.CUSTOM, label: window.t('background_tasks.date_filter.CUSTOM') }
];
},
);
},
+ refresh(e) {
+ e.preventDefault();
+ this.props.refresh();
+ let btn = e.target;
+ btn.disabled = true;
+ setTimeout(() => btn.disabled = false, 500);
+ },
+
render() {
return (
<section className="big-spacer-top big-spacer-bottom">
{this.renderCustomDateInput()}
</li>
<li>{this.renderSearchBox()}</li>
+ <li className="pull-right">
+ <button onClick={this.refresh} ref="reloadButton">{window.t('reload')}</button>
+ </li>
</ul>
</section>
);
import React from 'react';
+import {formatDuration} from './helpers';
export default React.createClass({
onPendingCanceled(e) {
return null;
}
return (
- <span className="huge-spacer-left">
+ <span className="huge-spacer-left" title={window.t('background_tasks.in_progress_duration')}
+ data-toggle="tooltip">
<i className="spinner spacer-right" style={{ verticalAlign: 'text-top' }}/>
- <span className="emphasised-measure">{this.props.inProgressDuration} ms</span>
+ <span ref="inProgressDuration" className="emphasised-measure">
+ {formatDuration(this.props.inProgressDuration)}
+ </span>
</span>
);
},
if (this.props.pendingCount > 0) {
return (
<span>
- <span className="emphasised-measure">{this.props.pendingCount}</span> pending
- <a onClick={this.onPendingCanceled} className="icon-delete spacer-left" title="Cancel Pending Tasks"
- data-toggle="tooltip" href="#"></a>
+ <span ref="pendingCount" className="emphasised-measure">{this.props.pendingCount}</span>
+
+ {window.t('background_tasks.pending')}
+ <a ref="cancelPending" onClick={this.onPendingCanceled} className="icon-delete spacer-left"
+ title={window.t('background_tasks.cancel_all_tasks')} data-toggle="tooltip" href="#"></a>
</span>
);
} else {
- return <span><span className="emphasised-measure">{this.props.pendingCount}</span> pending</span>;
+ return (
+ <span>
+ <span ref="pendingCount" className="emphasised-measure">{this.props.pendingCount}</span>
+
+ {window.t('background_tasks.pending')}
+ </span>
+ );
}
},
return null;
}
if (this.props.failuresCount > 0) {
- return <span><a onClick={this.onFailuresClick} className="emphasised-measure" href="#">{this.props.failuresCount}</a> failures</span>;
+ return (
+ <span>
+ <a ref="failureCount" onClick={this.onFailuresClick} className="emphasised-measure"
+ href="#">{this.props.failuresCount}</a>
+
+ {window.t('background_tasks.failures')}
+ </span>
+ );
} else {
- return <span><span className="emphasised-measure">{this.props.failuresCount}</span> failures</span>;
+ return (
+ <span>
+ <span ref="failureCount" className="emphasised-measure">{this.props.failuresCount}</span>
+
+ {window.t('background_tasks.failures')}
+ </span>
+ );
}
},
+import moment from 'moment';
import React from 'react';
import {getProjectUrl} from '../../helpers/Url';
import QualifierIcon from '../../components/shared/qualifier-icon';
import PendingIcon from '../../components/shared/pending-icon';
import {STATUSES} from './constants';
+import {formatDuration} from './helpers';
export default React.createClass({
propTypes: {
default:
inner = '';
}
- return <td className="thin spacer-right">{inner}</td>;
+ return <td className="thin spacer-right" title={window.t('background_task.status', task.status)}
+ data-toggle="tooltip">{inner}</td>;
},
renderTaskComponent(task) {
return <td><span className="note">{task.id}</span></td>;
}
- let qualifier = task.type === 'REPORT' ? 'TRK' : null;
return (
<td>
<span className="little-spacer-right">
- <QualifierIcon qualifier={qualifier}/>
+ <QualifierIcon qualifier={task.componentQualifier}/>
</span>
<a href={getProjectUrl(task.componentKey)}>{task.componentName}</a>
</td>
},
renderTaskExecutionTime(task) {
- let inner = task.executionTimeMs ? `${task.executionTimeMs} ms` : '';
- return <td className="thin nowrap text-right">{inner}</td>;
+ return <td className="thin nowrap text-right">{formatDuration(task.executionTimeMs)}</td>;
},
isAnotherDay(a, b) {
renderCancelButton(task) {
if (task.status === STATUSES.PENDING) {
return (
- <a onClick={this.onTaskCanceled.bind(this, task)} className="icon-delete" title="Cancel Task"
- data-toggle="tooltip" href="#"></a>
+ <a onClick={this.onTaskCanceled.bind(this, task)} className="icon-delete"
+ title={window.t('background_tasks.cancel_task')} data-toggle="tooltip" href="#"></a>
);
} else {
return null;
renderLogsLink(task) {
if (task.logs) {
let url = `${window.baseUrl}/api/ce/logs?taskId=${task.id}`;
- return <a target="_blank" href={url}>Logs</a>;
+ return <a target="_blank" href={url}>{window.t('background_tasks.logs')}</a>;
} else {
return null;
}
<th> </th>
<th> </th>
<th> </th>
- <th>Submitted</th>
- <th>Started</th>
- <th>Finished</th>
- <th>Duration</th>
+ <th>{window.t('background_tasks.table.submitted')}</th>
+ <th>{window.t('background_tasks.table.started')}</th>
+ <th>{window.t('background_tasks.table.finished')}</th>
+ <th>{window.t('background_tasks.table.duration')}</th>
<th> </th>
</tr>
</thead>
renderBackgroundTasksLink() {
// TODO check permissions
const url = `/project/background_tasks?id=${encodeURIComponent(this.props.component.key)}`;
- return this.renderLink(url, 'Background Tasks', '/project/background_tasks');
+ return this.renderLink(url, window.t('background_tasks.page'), '/project/background_tasks');
},
renderUpdateKeyLink() {
</a>
<ul className="dropdown-menu">
{this.renderLink('/projects', 'Management')}
- {this.renderLink('/background_tasks', 'Background Tasks')}
+ {this.renderLink('/background_tasks', window.t('background_tasks.page'))}
</ul>
</li>
--- /dev/null
+define(function () {
+ return window.moment;
+});
'underscore': 'libs/third-party/shim/underscore-shim',
'jquery': 'libs/third-party/shim/jquery-shim',
'backbone': 'libs/third-party/shim/backbone-shim',
- 'backbone.marionette': 'libs/third-party/shim/marionette-shim'
+ 'backbone.marionette': 'libs/third-party/shim/marionette-shim',
+ 'moment': 'libs/third-party/shim/moment-shim'
}
});
</script>
+/* eslint no-unused-expressions: 0 */
import React from 'react/addons';
import App from '../../src/main/js/apps/background-tasks/app';
import Header from '../../src/main/js/apps/background-tasks/header';
+import Stats from '../../src/main/js/apps/background-tasks/stats';
import Search from '../../src/main/js/apps/background-tasks/search';
+import Tasks from '../../src/main/js/apps/background-tasks/tasks';
import {STATUSES, CURRENTS, DEBOUNCE_DELAY} from '../../src/main/js/apps/background-tasks/constants';
+import {formatDuration} from '../../src/main/js/apps/background-tasks/helpers';
let TestUtils = React.addons.TestUtils;
let chai = require('chai');
done();
}, DEBOUNCE_DELAY);
});
+
+ it('should reload', () => {
+ let spy = sinon.spy(),
+ reloadSpy = sinon.spy();
+ let component = TestUtils.renderIntoDocument(<Search options={{}}
+ onStatusChange={spy}
+ onCurrentsChange={spy}
+ onDateChange={spy}
+ refresh={reloadSpy}/>);
+ let reloadButton = React.findDOMNode(component.refs.reloadButton);
+ expect(reloadSpy).to.not.have.been.called;
+ TestUtils.Simulate.click(reloadButton);
+ expect(reloadSpy).to.have.been.called;
+ });
+ });
+
+ describe('Stats', () => {
+ describe('Pending', () => {
+ it('should show zero pending', () => {
+ let result = TestUtils.renderIntoDocument(<Stats pendingCount="0"/>),
+ pendingCounter = React.findDOMNode(result.refs.pendingCount);
+ expect(pendingCounter.textContent).to.contain('0');
+ });
+
+ it('should show 5 pending', () => {
+ let result = TestUtils.renderIntoDocument(<Stats pendingCount="5"/>),
+ pendingCounter = React.findDOMNode(result.refs.pendingCount);
+ expect(pendingCounter.textContent).to.contain('5');
+ });
+
+ it('should not show cancel pending button', () => {
+ let result = TestUtils.renderIntoDocument(<Stats pendingCount="0"/>),
+ cancelPending = React.findDOMNode(result.refs.cancelPending);
+ expect(cancelPending).to.not.be.ok;
+ });
+
+ it('should show cancel pending button', () => {
+ let result = TestUtils.renderIntoDocument(<Stats pendingCount="5"/>),
+ cancelPending = React.findDOMNode(result.refs.cancelPending);
+ expect(cancelPending).to.be.ok;
+ });
+
+ it('should trigger cancelling pending', () => {
+ let spy = sinon.spy();
+ let result = TestUtils.renderIntoDocument(<Stats pendingCount="5" cancelPending={spy}/>),
+ cancelPending = React.findDOMNode(result.refs.cancelPending);
+ expect(spy).to.not.have.been.called;
+ TestUtils.Simulate.click(cancelPending);
+ expect(spy).to.have.been.called;
+ });
+ });
+
+ describe('Failures', () => {
+ it('should show zero failures', () => {
+ let result = TestUtils.renderIntoDocument(<Stats failuresCount="0"/>),
+ failureCounter = React.findDOMNode(result.refs.failureCount);
+ expect(failureCounter.textContent).to.contain('0');
+ });
+
+ it('should show 5 failures', () => {
+ let result = TestUtils.renderIntoDocument(<Stats failuresCount="5"/>),
+ failureCounter = React.findDOMNode(result.refs.failureCount);
+ expect(failureCounter.textContent).to.contain('5');
+ });
+
+ it('should not show link to failures', () => {
+ let result = TestUtils.renderIntoDocument(<Stats failuresCount="0"/>),
+ failureCounter = React.findDOMNode(result.refs.failureCount);
+ expect(failureCounter.tagName.toLowerCase()).to.not.equal('a');
+ });
+
+ it('should show link to failures', () => {
+ let result = TestUtils.renderIntoDocument(<Stats failuresCount="5"/>),
+ failureCounter = React.findDOMNode(result.refs.failureCount);
+ expect(failureCounter.tagName.toLowerCase()).to.equal('a');
+ });
+
+ it('should trigger filtering failures', () => {
+ let spy = sinon.spy();
+ let result = TestUtils.renderIntoDocument(<Stats failuresCount="5" showFailures={spy}/>),
+ failureCounter = React.findDOMNode(result.refs.failureCount);
+ expect(spy).to.not.have.been.called;
+ TestUtils.Simulate.click(failureCounter);
+ expect(spy).to.have.been.called;
+ });
+ });
+
+ describe('In Progress Duration', () => {
+ it('should show duration', () => {
+ let result = TestUtils.renderIntoDocument(<Stats inProgressDuration="173"/>),
+ inProgressDuration = React.findDOMNode(result.refs.inProgressDuration);
+ expect(inProgressDuration.textContent).to.include('173ms');
+ });
+
+ it('should format duration', () => {
+ let result = TestUtils.renderIntoDocument(<Stats inProgressDuration="1073"/>),
+ inProgressDuration = React.findDOMNode(result.refs.inProgressDuration);
+ expect(inProgressDuration.textContent).to.include('1s');
+ });
+
+ it('should not show duration', () => {
+ let result = TestUtils.renderIntoDocument(<Stats/>),
+ inProgressDuration = React.findDOMNode(result.refs.inProgressDuration);
+ expect(inProgressDuration).to.not.be.ok;
+ });
+ });
+ });
+
+ describe('Tasks', () => {
+ it('should show list', () => {
+ let tasks = [
+ { id: 'a' },
+ { id: 'b' },
+ { id: 'c' }
+ ];
+ let result = TestUtils.renderIntoDocument(<Tasks tasks={tasks}/>);
+ expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'tr')).to.have.length(3 + /* table header */ 1);
+ });
+ });
+
+ describe('Helpers', () => {
+ describe('#formatDuration()', () => {
+ it('should format 173ms', () => {
+ expect(formatDuration(173)).to.equal('173ms');
+ });
+
+ it('should format 999ms', () => {
+ expect(formatDuration(999)).to.equal('999ms');
+ });
+
+ it('should format 1s', () => {
+ expect(formatDuration(1000)).to.equal('1s');
+ });
+
+ it('should format 1s', () => {
+ expect(formatDuration(1001)).to.equal('1s');
+ });
+
+ it('should format 2s', () => {
+ expect(formatDuration(1501)).to.equal('2s');
+ });
+
+ it('should format 59s', () => {
+ expect(formatDuration(59000)).to.equal('59s');
+ });
+
+ it('should format 1min', () => {
+ expect(formatDuration(60000)).to.equal('1min');
+ });
+
+ it('should format 1min', () => {
+ expect(formatDuration(62757)).to.equal('1min');
+ });
+
+ it('should format 4min', () => {
+ expect(formatDuration(224567)).to.equal('4min');
+ });
+
+ it('should format 80min', () => {
+ expect(formatDuration(80 * 60 * 1000)).to.equal('80min');
+ });
+ });
});
});
component_navigation.status.in_progress=The analysis is in progress.
component_navigation.status.in_progress.admin=The analysis is in progress.<br>More details available on the <a href="{0}">Background Tasks</a> page.
+background_task.status.ALL=All
background_task.status.PENDING=Pending
background_task.status.IN_PROGRESS=In Progress
background_task.status.SUCCESS=Success
background_task.status.FAILED=Failed
background_task.status.CANCELED=Canceled
+background_tasks.page=Background Tasks
+background_tasks.page.description=The server is in charge to process reports submitted by batch analyses. This page allows to monitor the queue of pending reports to process, and gives access to the history of past analyses.
+background_tasks.currents_filter.ALL=All
+background_tasks.currents_filter.ONLY_CURRENTS=Only Latest Analysis
+
+background_tasks.date_filter.ALL=Any Date
+background_tasks.date_filter.TODAY=Today
+background_tasks.date_filter.CUSTOM=Custom
+
+background_tasks.table.submitted=Submitted
+background_tasks.table.started=Started
+background_tasks.table.finished=Finished
+background_tasks.table.duration=Duration
+
+background_tasks.logs=Logs
+background_tasks.cancel_task=Cancel Task
+background_tasks.cancel_all_tasks=Cancel All Pending Tasks
+background_tasks.pending=pending
+background_tasks.failures=failures
+background_tasks.in_progress_duration=Duration of the current task in progress.