feat(xo-web/logs): display real job status (#2688)

This commit is contained in:
Julien Fontanet 2018-02-26 18:02:39 +01:00 committed by GitHub
parent 80e66415d7
commit ee47e40d1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 165 additions and 164 deletions

View File

@ -70,13 +70,16 @@ export default class JobExecutor {
)
}
async exec (job) {
async exec (job, onStart) {
const runJobId = this._logger.notice(`Starting execution of ${job.id}.`, {
event: 'job.start',
userId: job.userId,
jobId: job.id,
key: job.key,
})
if (onStart !== undefined) {
onStart(runJobId)
}
try {
if (job.type === 'call') {

View File

@ -30,7 +30,12 @@ export default class Jobs {
}
async getAllJobs () {
return /* await */ this._jobs.get()
const jobs = await this._jobs.get()
const runningJobs = this._runningJobs
jobs.forEach(job => {
job.runId = runningJobs[job.id]
})
return jobs
}
async getJob (id) {
@ -69,8 +74,11 @@ export default class Jobs {
if (runningJobs[id]) {
throw new Error(`job ${id} is already running`)
}
runningJobs[id] = true
return this._executor.exec(job)::lastly(() => {
return this._executor
.exec(job, runJobId => {
runningJobs[id] = runJobId
})
::lastly(() => {
delete runningJobs[id]
})
}

View File

@ -274,8 +274,9 @@ const messages = {
jobServerTimezone: 'Server',
runJob: 'Run job',
runJobVerbose: 'One shot running started. See overview for logs.',
jobStarted: 'Started',
jobFinished: 'Finished',
jobInterrupted: 'Interrupted',
jobStarted: 'Started',
saveBackupJob: 'Save',
deleteBackupSchedule: 'Remove backup job',
deleteBackupScheduleQuestion:

View File

@ -1866,10 +1866,28 @@ export const createSrLvm = (host, nameLabel, nameDescription, device) =>
// Job logs ----------------------------------------------------------
export const deleteJobsLog = id =>
_call('log.delete', { namespace: 'jobs', id })::tap(
subscribeJobsLogs.forceRefresh
)
export const deleteJobsLogs = async ids => {
const { length } = ids
if (length === 0) {
return
}
if (length !== 1) {
const vars = { nLogs: length }
try {
await confirm({
title: _('logDeleteMultiple', vars),
body: <p>{_('logDeleteMultipleMessage', vars)}</p>,
})
} catch (_) {
return
}
}
return _call('log.delete', {
namespace: 'jobs',
id: ids.map(resolveId),
})::tap(subscribeJobsLogs.forceRefresh)
}
// Logs

View File

@ -1,8 +1,6 @@
import _, { FormattedDuration } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import addSubscriptions from 'add-subscriptions'
import BaseComponent from 'base-component'
import ButtonGroup from 'button-group'
import classnames from 'classnames'
import Icon from 'icon'
import NoObjects from 'no-objects'
@ -12,13 +10,14 @@ import renderXoItem from 'render-xo-item'
import Select from 'form/select'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { alert, confirm } from 'modal'
import { alert } from 'modal'
import { Card, CardHeader, CardBlock } from 'card'
import { connectStore, formatSize, formatSpeed } from 'utils'
import { createFilter, createGetObject, createSelector } from 'selectors'
import { deleteJobsLog, subscribeJobsLogs } from 'xo'
import { forEach, get, includes, isEmpty, map, orderBy } from 'lodash'
import { deleteJobsLogs, subscribeJobs, subscribeJobsLogs } from 'xo'
import { forEach, includes, keyBy, map, orderBy } from 'lodash'
import { FormattedDate } from 'react-intl'
import { get } from 'xo-defined'
// ===================================================================
@ -270,10 +269,28 @@ class Log extends BaseComponent {
const showCalls = log =>
alert(_('jobModalTitle', { job: log.jobId }), <Log log={log} />)
const LOG_ACTIONS = [
{
handler: deleteJobsLogs,
icon: 'delete',
label: _('remove'),
},
]
const LOG_ACTIONS_INDIVIDUAL = [
{
handler: showCalls,
icon: 'preview',
label: _('logDisplayDetails'),
},
]
const getCallTag = log => log.calls[0].params.tag
const LOG_COLUMNS = [
{
name: _('jobId'),
itemRenderer: log => log.jobId,
itemRenderer: log => log.jobId.slice(4, 8),
sortCriteria: log => log.jobId,
},
{
@ -283,8 +300,8 @@ const LOG_COLUMNS = [
},
{
name: _('jobTag'),
itemRenderer: log => get(log, 'calls[0].params.tag'),
sortCriteria: log => get(log, 'calls[0].params.tag'),
itemRenderer: log => get(getCallTag, log),
sortCriteria: log => get(getCallTag, log),
},
{
name: _('jobStart'),
@ -329,9 +346,9 @@ const LOG_COLUMNS = [
},
{
name: _('jobStatus'),
itemRenderer: log => (
itemRenderer: (log, { jobs }) => (
<span>
{log.status === 'finished' && (
{log.status === 'finished' ? (
<span
className={classnames(
'tag',
@ -342,69 +359,42 @@ const LOG_COLUMNS = [
>
{_('jobFinished')}
</span>
)}
{log.status === 'started' && (
) : log.status === 'started' ? (
log.id === get(() => jobs[log.jobId].runId) ? (
<span className='tag tag-warning'>{_('jobStarted')}</span>
)}
{log.status !== 'started' &&
log.status !== 'finished' && (
) : (
<span className='tag tag-danger'>{_('jobInterrupted')}</span>
)
) : (
<span className='tag tag-default'>{_('jobUnknown')}</span>
)}{' '}
<span className='pull-right'>
<ButtonGroup>
<Tooltip content={_('logDisplayDetails')}>
<ActionRowButton
icon='preview'
handler={showCalls}
handlerParam={log}
/>
</Tooltip>
<Tooltip content={_('remove')}>
<ActionRowButton
handler={deleteJobsLog}
handlerParam={log.logKey}
icon='delete'
/>
</Tooltip>
</ButtonGroup>
</span>
)}
</span>
),
sortCriteria: log => (log.hasErrors ? ' ' : log.status),
},
]
@propTypes({
jobKeys: propTypes.array.isRequired,
})
export default class LogList extends Component {
constructor (props) {
super(props)
this.state = {
logsToClear: [],
}
this.filters = {
const LOG_FILTERS = {
onError: 'hasErrors?',
successful: 'status:finished !hasErrors?',
jobCallSkipped: '!hasErrors? callSkipped?',
}
}
}
componentWillMount () {
this.componentWillUnmount = subscribeJobsLogs(rawLogs => {
export default [
propTypes({
jobKeys: propTypes.array.isRequired,
}),
addSubscriptions(({ jobKeys }) => ({
logs: cb =>
subscribeJobsLogs(rawLogs => {
const logs = {}
const logsToClear = []
forEach(rawLogs, (log, logKey) => {
forEach(rawLogs, (log, id) => {
const data = log.data
const { time } = log
if (
data.event === 'job.start' &&
includes(this.props.jobKeys, data.key)
) {
logsToClear.push(logKey)
logs[logKey] = {
logKey,
jobId: data.jobId.slice(4, 8),
if (data.event === 'job.start' && includes(jobKeys, data.key)) {
logs[id] = {
id,
jobId: data.jobId,
key: data.key,
userId: data.userId,
start: time,
@ -417,14 +407,13 @@ export default class LogList extends Component {
if (!entry) {
return
}
logsToClear.push(logKey)
if (data.event === 'job.end') {
entry.end = time
entry.duration = time - entry.start
entry.status = 'finished'
} else if (data.event === 'jobCall.start') {
entry.calls[logKey] = {
callKey: logKey,
entry.calls[id] = {
callKey: id,
params: data.params,
method: data.method,
start: time,
@ -458,45 +447,27 @@ export default class LogList extends Component {
log.calls = orderBy(log.calls, ['time'], ['desc'])
})
this.setState({
logs: orderBy(logs, ['time'], ['desc']),
logsToClear,
})
})
}
_deleteAllLogs = () => {
return confirm({
title: _('removeAllLogsModalTitle'),
body: <p>{_('removeAllLogsModalWarning')}</p>,
}).then(() => deleteJobsLog(this.state.logsToClear))
}
render () {
const { logs } = this.state
return (
cb(orderBy(logs, ['time'], ['desc']))
}),
jobs: cb => subscribeJobs(jobs => cb(keyBy(jobs, 'id'))),
})),
({ logs, jobs }) => (
<Card>
<CardHeader>
<Icon icon='log' /> Logs<span className='pull-right'>
<ActionButton
disabled={isEmpty(logs)}
btnStyle='danger'
handler={this._deleteAllLogs}
icon='delete'
/>
</span>
<Icon icon='log' /> Logs
</CardHeader>
<CardBlock>
<NoObjects
actions={LOG_ACTIONS}
collection={logs}
columns={LOG_COLUMNS}
component={SortedTable}
data-jobs={jobs}
emptyMessage={_('noLogs')}
filters={this.filters}
filters={LOG_FILTERS}
individualActions={LOG_ACTIONS_INDIVIDUAL}
/>
</CardBlock>
</Card>
)
}
}
),
].reduceRight((value, decorator) => decorator(value))