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}.`, { const runJobId = this._logger.notice(`Starting execution of ${job.id}.`, {
event: 'job.start', event: 'job.start',
userId: job.userId, userId: job.userId,
jobId: job.id, jobId: job.id,
key: job.key, key: job.key,
}) })
if (onStart !== undefined) {
onStart(runJobId)
}
try { try {
if (job.type === 'call') { if (job.type === 'call') {

View File

@ -30,7 +30,12 @@ export default class Jobs {
} }
async getAllJobs () { 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) { async getJob (id) {
@ -69,8 +74,11 @@ export default class Jobs {
if (runningJobs[id]) { if (runningJobs[id]) {
throw new Error(`job ${id} is already running`) throw new Error(`job ${id} is already running`)
} }
runningJobs[id] = true return this._executor
return this._executor.exec(job)::lastly(() => { .exec(job, runJobId => {
runningJobs[id] = runJobId
})
::lastly(() => {
delete runningJobs[id] delete runningJobs[id]
}) })
} }

View File

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

View File

@ -1866,10 +1866,28 @@ export const createSrLvm = (host, nameLabel, nameDescription, device) =>
// Job logs ---------------------------------------------------------- // Job logs ----------------------------------------------------------
export const deleteJobsLog = id => export const deleteJobsLogs = async ids => {
_call('log.delete', { namespace: 'jobs', id })::tap( const { length } = ids
subscribeJobsLogs.forceRefresh 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 // Logs

View File

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