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}.`, {
|
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') {
|
||||||
|
@ -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,10 +74,13 @@ 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 => {
|
||||||
delete runningJobs[id]
|
runningJobs[id] = runJobId
|
||||||
})
|
})
|
||||||
|
::lastly(() => {
|
||||||
|
delete runningJobs[id]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async runJobSequence (idSequence) {
|
async runJobSequence (idSequence) {
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,161 +359,115 @@ const LOG_COLUMNS = [
|
|||||||
>
|
>
|
||||||
{_('jobFinished')}
|
{_('jobFinished')}
|
||||||
</span>
|
</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>
|
</span>
|
||||||
),
|
),
|
||||||
sortCriteria: log => (log.hasErrors ? ' ' : log.status),
|
sortCriteria: log => (log.hasErrors ? ' ' : log.status),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@propTypes({
|
const LOG_FILTERS = {
|
||||||
jobKeys: propTypes.array.isRequired,
|
onError: 'hasErrors?',
|
||||||
})
|
successful: 'status:finished !hasErrors?',
|
||||||
export default class LogList extends Component {
|
jobCallSkipped: '!hasErrors? callSkipped?',
|
||||||
constructor (props) {
|
}
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
logsToClear: [],
|
|
||||||
}
|
|
||||||
this.filters = {
|
|
||||||
onError: 'hasErrors?',
|
|
||||||
successful: 'status:finished !hasErrors?',
|
|
||||||
jobCallSkipped: '!hasErrors? callSkipped?',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount () {
|
export default [
|
||||||
this.componentWillUnmount = subscribeJobsLogs(rawLogs => {
|
propTypes({
|
||||||
const logs = {}
|
jobKeys: propTypes.array.isRequired,
|
||||||
const logsToClear = []
|
}),
|
||||||
forEach(rawLogs, (log, logKey) => {
|
addSubscriptions(({ jobKeys }) => ({
|
||||||
const data = log.data
|
logs: cb =>
|
||||||
const { time } = log
|
subscribeJobsLogs(rawLogs => {
|
||||||
if (
|
const logs = {}
|
||||||
data.event === 'job.start' &&
|
forEach(rawLogs, (log, id) => {
|
||||||
includes(this.props.jobKeys, data.key)
|
const data = log.data
|
||||||
) {
|
const { time } = log
|
||||||
logsToClear.push(logKey)
|
if (data.event === 'job.start' && includes(jobKeys, data.key)) {
|
||||||
logs[logKey] = {
|
logs[id] = {
|
||||||
logKey,
|
id,
|
||||||
jobId: data.jobId.slice(4, 8),
|
jobId: data.jobId,
|
||||||
key: data.key,
|
key: data.key,
|
||||||
userId: data.userId,
|
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,
|
|
||||||
start: time,
|
start: time,
|
||||||
|
calls: {},
|
||||||
time,
|
time,
|
||||||
}
|
}
|
||||||
} else if (data.event === 'jobCall.end') {
|
} else {
|
||||||
const call = entry.calls[data.runCallId]
|
const runJobId = data.runJobId
|
||||||
|
const entry = logs[runJobId]
|
||||||
if (data.error) {
|
if (!entry) {
|
||||||
call.error = data.error
|
return
|
||||||
if (isSkippedError(data.error)) {
|
}
|
||||||
entry.callSkipped = true
|
if (data.event === 'job.end') {
|
||||||
} else {
|
entry.end = time
|
||||||
entry.hasErrors = true
|
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 => {
|
forEach(logs, log => {
|
||||||
if (log.end === undefined) {
|
if (log.end === undefined) {
|
||||||
log.status = 'started'
|
log.status = 'started'
|
||||||
} else if (!log.meta) {
|
} else if (!log.meta) {
|
||||||
log.meta = 'success'
|
log.meta = 'success'
|
||||||
}
|
}
|
||||||
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 }) => (
|
||||||
}
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
_deleteAllLogs = () => {
|
<Icon icon='log' /> Logs
|
||||||
return confirm({
|
</CardHeader>
|
||||||
title: _('removeAllLogsModalTitle'),
|
<CardBlock>
|
||||||
body: <p>{_('removeAllLogsModalWarning')}</p>,
|
<NoObjects
|
||||||
}).then(() => deleteJobsLog(this.state.logsToClear))
|
actions={LOG_ACTIONS}
|
||||||
}
|
collection={logs}
|
||||||
|
columns={LOG_COLUMNS}
|
||||||
render () {
|
component={SortedTable}
|
||||||
const { logs } = this.state
|
data-jobs={jobs}
|
||||||
|
emptyMessage={_('noLogs')}
|
||||||
return (
|
filters={LOG_FILTERS}
|
||||||
<Card>
|
individualActions={LOG_ACTIONS_INDIVIDUAL}
|
||||||
<CardHeader>
|
/>
|
||||||
<Icon icon='log' /> Logs<span className='pull-right'>
|
</CardBlock>
|
||||||
<ActionButton
|
</Card>
|
||||||
disabled={isEmpty(logs)}
|
),
|
||||||
btnStyle='danger'
|
].reduceRight((value, decorator) => decorator(value))
|
||||||
handler={this._deleteAllLogs}
|
|
||||||
icon='delete'
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBlock>
|
|
||||||
<NoObjects
|
|
||||||
collection={logs}
|
|
||||||
columns={LOG_COLUMNS}
|
|
||||||
component={SortedTable}
|
|
||||||
emptyMessage={_('noLogs')}
|
|
||||||
filters={this.filters}
|
|
||||||
/>
|
|
||||||
</CardBlock>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user