From ee47e40d1a7d146aecabb4ccc16ffbaae81a3148 Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Mon, 26 Feb 2018 18:02:39 +0100 Subject: [PATCH] feat(xo-web/logs): display real job status (#2688) --- packages/xo-server/src/job-executor.js | 5 +- packages/xo-server/src/xo-mixins/jobs.js | 18 +- packages/xo-web/src/common/intl/messages.js | 3 +- packages/xo-web/src/common/xo/index.js | 26 +- packages/xo-web/src/xo-app/logs/index.js | 277 +++++++++----------- 5 files changed, 165 insertions(+), 164 deletions(-) diff --git a/packages/xo-server/src/job-executor.js b/packages/xo-server/src/job-executor.js index 6a0e45e6e..4f5457d62 100644 --- a/packages/xo-server/src/job-executor.js +++ b/packages/xo-server/src/job-executor.js @@ -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') { diff --git a/packages/xo-server/src/xo-mixins/jobs.js b/packages/xo-server/src/xo-mixins/jobs.js index 40dfa4d29..af5ffb3a0 100644 --- a/packages/xo-server/src/xo-mixins/jobs.js +++ b/packages/xo-server/src/xo-mixins/jobs.js @@ -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) { diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 4f6e10abc..e6a7d7384 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -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: diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index cc936bdb4..9939ca812 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -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:

{_('logDeleteMultipleMessage', vars)}

, + }) + } catch (_) { + return + } + } + + return _call('log.delete', { + namespace: 'jobs', + id: ids.map(resolveId), + })::tap(subscribeJobsLogs.forceRefresh) +} // Logs diff --git a/packages/xo-web/src/xo-app/logs/index.js b/packages/xo-web/src/xo-app/logs/index.js index 9f572a3d8..7a66cda77 100644 --- a/packages/xo-web/src/xo-app/logs/index.js +++ b/packages/xo-web/src/xo-app/logs/index.js @@ -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 }), ) +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 }) => ( - {log.status === 'finished' && ( + {log.status === 'finished' ? ( {_('jobFinished')} + ) : log.status === 'started' ? ( + log.id === get(() => jobs[log.jobId].runId) ? ( + {_('jobStarted')} + ) : ( + {_('jobInterrupted')} + ) + ) : ( + {_('jobUnknown')} )} - {log.status === 'started' && ( - {_('jobStarted')} - )} - {log.status !== 'started' && - log.status !== 'finished' && ( - {_('jobUnknown')} - )}{' '} - - - - - - - - - - ), 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:

{_('removeAllLogsModalWarning')}

, - }).then(() => deleteJobsLog(this.state.logsToClear)) - } - - render () { - const { logs } = this.state - - return ( - - - Logs - - - - - - - - ) - } -} + cb(orderBy(logs, ['time'], ['desc'])) + }), + jobs: cb => subscribeJobs(jobs => cb(keyBy(jobs, 'id'))), + })), + ({ logs, jobs }) => ( + + + Logs + + + + + + ), +].reduceRight((value, decorator) => decorator(value))