feat(xo-web/logs): display real job status (#2688)
This commit is contained in:
parent
80e66415d7
commit
ee47e40d1a
@ -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') {
|
||||
|
@ -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,10 +74,13 @@ export default class Jobs {
|
||||
if (runningJobs[id]) {
|
||||
throw new Error(`job ${id} is already running`)
|
||||
}
|
||||
runningJobs[id] = true
|
||||
return this._executor.exec(job)::lastly(() => {
|
||||
delete runningJobs[id]
|
||||
})
|
||||
return this._executor
|
||||
.exec(job, runJobId => {
|
||||
runningJobs[id] = runJobId
|
||||
})
|
||||
::lastly(() => {
|
||||
delete runningJobs[id]
|
||||
})
|
||||
}
|
||||
|
||||
async runJobSequence (idSequence) {
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,161 +359,115 @@ const LOG_COLUMNS = [
|
||||
>
|
||||
{_('jobFinished')}
|
||||
</span>
|
||||
) : log.status === 'started' ? (
|
||||
log.id === get(() => jobs[log.jobId].runId) ? (
|
||||
<span className='tag tag-warning'>{_('jobStarted')}</span>
|
||||
) : (
|
||||
<span className='tag tag-danger'>{_('jobInterrupted')}</span>
|
||||
)
|
||||
) : (
|
||||
<span className='tag tag-default'>{_('jobUnknown')}</span>
|
||||
)}
|
||||
{log.status === 'started' && (
|
||||
<span className='tag tag-warning'>{_('jobStarted')}</span>
|
||||
)}
|
||||
{log.status !== 'started' &&
|
||||
log.status !== 'finished' && (
|
||||
<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 = {
|
||||
onError: 'hasErrors?',
|
||||
successful: 'status:finished !hasErrors?',
|
||||
jobCallSkipped: '!hasErrors? callSkipped?',
|
||||
}
|
||||
}
|
||||
const LOG_FILTERS = {
|
||||
onError: 'hasErrors?',
|
||||
successful: 'status:finished !hasErrors?',
|
||||
jobCallSkipped: '!hasErrors? callSkipped?',
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeJobsLogs(rawLogs => {
|
||||
const logs = {}
|
||||
const logsToClear = []
|
||||
forEach(rawLogs, (log, logKey) => {
|
||||
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),
|
||||
key: data.key,
|
||||
userId: data.userId,
|
||||
start: time,
|
||||
calls: {},
|
||||
time,
|
||||
}
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
const entry = logs[runJobId]
|
||||
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,
|
||||
params: data.params,
|
||||
method: data.method,
|
||||
export default [
|
||||
propTypes({
|
||||
jobKeys: propTypes.array.isRequired,
|
||||
}),
|
||||
addSubscriptions(({ jobKeys }) => ({
|
||||
logs: cb =>
|
||||
subscribeJobsLogs(rawLogs => {
|
||||
const logs = {}
|
||||
forEach(rawLogs, (log, id) => {
|
||||
const data = log.data
|
||||
const { time } = log
|
||||
if (data.event === 'job.start' && includes(jobKeys, data.key)) {
|
||||
logs[id] = {
|
||||
id,
|
||||
jobId: data.jobId,
|
||||
key: data.key,
|
||||
userId: data.userId,
|
||||
start: time,
|
||||
calls: {},
|
||||
time,
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
if (isSkippedError(data.error)) {
|
||||
entry.callSkipped = true
|
||||
} else {
|
||||
entry.hasErrors = true
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
const entry = logs[runJobId]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
if (data.event === 'job.end') {
|
||||
entry.end = time
|
||||
entry.duration = time - entry.start
|
||||
entry.status = 'finished'
|
||||
} else if (data.event === 'jobCall.start') {
|
||||
entry.calls[id] = {
|
||||
callKey: id,
|
||||
params: data.params,
|
||||
method: data.method,
|
||||
start: time,
|
||||
time,
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
if (isSkippedError(data.error)) {
|
||||
entry.callSkipped = true
|
||||
} else {
|
||||
entry.hasErrors = true
|
||||
}
|
||||
entry.meta = 'error'
|
||||
} else {
|
||||
call.returnedValue = data.returnedValue
|
||||
call.end = time
|
||||
}
|
||||
entry.meta = 'error'
|
||||
} else {
|
||||
call.returnedValue = data.returnedValue
|
||||
call.end = time
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
forEach(logs, log => {
|
||||
if (log.end === undefined) {
|
||||
log.status = 'started'
|
||||
} else if (!log.meta) {
|
||||
log.meta = 'success'
|
||||
}
|
||||
log.calls = orderBy(log.calls, ['time'], ['desc'])
|
||||
})
|
||||
forEach(logs, log => {
|
||||
if (log.end === undefined) {
|
||||
log.status = 'started'
|
||||
} else if (!log.meta) {
|
||||
log.meta = 'success'
|
||||
}
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='log' /> Logs<span className='pull-right'>
|
||||
<ActionButton
|
||||
disabled={isEmpty(logs)}
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
/>
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
collection={logs}
|
||||
columns={LOG_COLUMNS}
|
||||
component={SortedTable}
|
||||
emptyMessage={_('noLogs')}
|
||||
filters={this.filters}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
cb(orderBy(logs, ['time'], ['desc']))
|
||||
}),
|
||||
jobs: cb => subscribeJobs(jobs => cb(keyBy(jobs, 'id'))),
|
||||
})),
|
||||
({ logs, jobs }) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='log' /> Logs
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={LOG_ACTIONS}
|
||||
collection={logs}
|
||||
columns={LOG_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-jobs={jobs}
|
||||
emptyMessage={_('noLogs')}
|
||||
filters={LOG_FILTERS}
|
||||
individualActions={LOG_ACTIONS_INDIVIDUAL}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
|
Loading…
Reference in New Issue
Block a user