feat(xo-web/backup-ng/logs): move restore logs to the restore tab (#3802)

Fixes #3772
This commit is contained in:
badrAZ 2018-12-20 17:17:33 +01:00 committed by Pierre Donias
parent c55daae734
commit 841a8ed1a5
6 changed files with 255 additions and 242 deletions

View File

@ -4,6 +4,8 @@
### Enhancements
- [Backup NG] Restore logs moved to restore tab [#3772](https://github.com/vatesfr/xen-orchestra/issues/3772) (PR [#3802](https://github.com/vatesfr/xen-orchestra/pull/3802))
### Bug fixes
### Released packages

View File

@ -199,9 +199,7 @@ const messages = {
stateEnabled: 'Enabled',
// ----- Labels -----
labelBackup: 'Backup',
labelMerge: 'Merge',
labelRestore: 'Restore',
labelSize: 'Size',
labelSpeed: 'Speed',
labelSr: 'SR',
@ -1830,7 +1828,8 @@ const messages = {
logsTenPerPage: '10 / page',
logsJobId: 'Job ID',
logsJobName: 'Job name',
logsJobTime: 'Job time',
logsBackupTime: 'Backup time',
logsRestoreTime: 'Restore time',
logsVmNotFound: 'VM not found!',
logsMissingVms: 'Missing VMs skipped ({ vms })',
logsFailedRestoreError: 'Click to show error',

View File

@ -35,6 +35,8 @@ import DeleteBackupsModalBody from './delete-backups-modal-body'
import RestoreLegacy from '../restore-legacy'
import Logs from '../../logs/restore'
// -----------------------------------------------------------------------------
const BACKUPS_COLUMNS = [
@ -270,6 +272,8 @@ export default class Restore extends Component {
collection={this.state.backupDataByVm}
columns={BACKUPS_COLUMNS}
/>
<br />
<Logs />
<RestoreLegacy />
</div>
</Upgrade>

View File

@ -1,28 +1,23 @@
import _, { FormattedDuration } from 'intl'
import ActionButton from 'action-button'
import addSubscriptions from 'add-subscriptions'
import Button from 'button'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
import defined, { get } from '@xen-orchestra/defined'
import Icon from 'icon'
import NoObjects from 'no-objects'
import React from 'react'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { alert } from 'modal'
import { Card, CardHeader, CardBlock } from 'card'
import { connectStore, formatSize, formatSpeed } from 'utils'
import { connectStore, formatSize } from 'utils'
import { createGetObjectsOfType } from 'selectors'
import { FormattedDate } from 'react-intl'
import { get } from '@xen-orchestra/defined'
import { injectState, provideState } from 'reaclette'
import { isEmpty, groupBy, map, keyBy } from 'lodash'
import { isEmpty, filter, map, keyBy } from 'lodash'
import { subscribeBackupNgJobs, subscribeBackupNgLogs } from 'xo'
import { toggleState } from 'reaclette-utils'
import { Vm, Sr } from 'render-xo-item'
import LogAlertBody from './log-alert-body'
import LogAlertHeader from './log-alert-header'
import { STATUS_LABELS, LOG_FILTERS, LogDate } from './utils'
const UL_STYLE = { listStyleType: 'none' }
@ -30,29 +25,6 @@ const LI_STYLE = {
whiteSpace: 'nowrap',
}
const STATUS_LABELS = {
failure: {
className: 'danger',
label: 'jobFailed',
},
skipped: {
className: 'info',
label: 'jobSkipped',
},
success: {
className: 'success',
label: 'jobSuccess',
},
pending: {
className: 'warning',
label: 'jobStarted',
},
interrupted: {
className: 'danger',
label: 'jobInterrupted',
},
}
const showTasks = id =>
alert(<LogAlertHeader id={id} />, <LogAlertBody id={id} />)
@ -72,28 +44,7 @@ export const LogStatus = ({ log, tooltip = _('logDisplayDetails') }) => {
)
}
const LogDate = ({ time }) => (
<FormattedDate
value={new Date(time)}
month='short'
day='numeric'
year='numeric'
hour='2-digit'
minute='2-digit'
second='2-digit'
/>
)
const DURATION_COLUMN = {
name: _('jobDuration'),
itemRenderer: log =>
log.end !== undefined && (
<FormattedDuration duration={log.end - log.start} />
),
sortCriteria: log => log.end - log.start,
}
const LOG_BACKUP_COLUMNS = [
const COLUMNS = [
{
name: _('jobId'),
itemRenderer: log => log.jobId.slice(4, 8),
@ -117,7 +68,14 @@ const LOG_BACKUP_COLUMNS = [
sortCriteria: log => log.end || log.start,
sortOrder: 'desc',
},
DURATION_COLUMN,
{
name: _('jobDuration'),
itemRenderer: log =>
log.end !== undefined && (
<FormattedDuration duration={log.end - log.start} />
),
sortCriteria: log => log.end - log.start,
},
{
name: _('jobStatus'),
itemRenderer: log => <LogStatus log={log} />,
@ -182,169 +140,22 @@ const LOG_BACKUP_COLUMNS = [
},
]
const showRestoreError = ({ currentTarget: { dataset } }) =>
alert(
_('logsFailedRestoreTitle'),
<Copiable data={dataset.error} className='text-danger' tagName='div'>
<Icon icon='alarm' /> {dataset.message}
</Copiable>
)
const LOG_RESTORE_COLUMNS = [
{
name: _('logsJobId'),
itemRenderer: ({ data: { jobId } }) => jobId.slice(4, 8),
sortCriteria: 'data.jobId',
},
{
name: _('logsJobName'),
itemRenderer: ({ data: { jobId } }, { jobs }) =>
get(() => jobs[jobId].name),
sortCriteria: ({ data: { jobId } }, { jobs }) =>
get(() => jobs[jobId].name),
},
{
name: _('logsJobTime'),
itemRenderer: ({ data: { time } }) => <LogDate time={time} />,
sortCriteria: 'data.time',
},
{
name: _('labelVm'),
itemRenderer: ({ id, vm, status }) => (
<div>
{vm !== undefined && <Vm id={vm.id} link newTab />}
{vm === undefined && status === 'success' && (
<span className='text-warning'>{_('logsVmNotFound')}</span>
)}{' '}
<span style={{ fontSize: '0.5em' }} className='text-muted'>
{id}
</span>
</div>
),
sortCriteria: ({ vm }) => vm !== undefined && vm.name_label,
},
{
default: true,
name: _('jobStart'),
itemRenderer: log => <LogDate time={log.start} />,
sortCriteria: 'start',
sortOrder: 'desc',
},
DURATION_COLUMN,
{
name: _('labelSr'),
itemRenderer: ({ data: { srId } }) => <Sr id={srId} link newTab />,
sortCriteria: ({ data: { srId } }, { srs }) =>
get(() => srs[srId].name_label),
},
{
name: _('jobStatus'),
itemRenderer: task => {
const { className, label } = STATUS_LABELS[task.status]
return (
<div>
<span className={`tag tag-${className}`}>{_(label)}</span>{' '}
{task.status === 'failure' && (
<Tooltip content={_('logsFailedRestoreError')}>
<a
className='text-danger'
onClick={showRestoreError}
data-message={task.result.message}
data-error={JSON.stringify(task.result)}
style={{ cursor: 'pointer' }}
>
<Icon icon='alarm' />
</a>
</Tooltip>
)}
</div>
)
},
sortCriteria: 'status',
},
{
name: _('labelSize'),
itemRenderer: ({ dataSize }) =>
dataSize !== undefined && formatSize(dataSize),
sortCriteria: 'dataSize',
},
{
name: _('labelSpeed'),
itemRenderer: task => {
const duration = task.end - task.start
return (
task.dataSize !== undefined &&
duration > 0 &&
formatSpeed(task.dataSize, duration)
)
},
sortCriteria: task => {
const duration = task.end - task.start
return (
task.dataSize !== undefined && duration > 0 && task.dataSize / duration
)
},
},
]
const ROW_TRANSFORM = (task, { vms }) => {
let vm, dataSize
if (task.status === 'success') {
const result = task.tasks.find(({ message }) => message === 'transfer')
.result
dataSize = result.size
vm = vms && vms[result.id]
}
return {
...task,
dataSize,
vm,
}
}
const LOG_FILTERS = {
jobFailed: 'status: failure',
jobInterrupted: 'status: interrupted',
jobSkipped: 'status: skipped',
jobStarted: 'status: pending',
jobSuccess: 'status: success',
}
const TenPerPage = ({ name, handler, value }) => (
<Button className='pull-right' name={name} onClick={handler} size='small'>
{_(value ? 'logsThreePerPage' : 'logsTenPerPage')}
</Button>
)
export default decorate([
connectStore({
srs: createGetObjectsOfType('SR'),
vms: createGetObjectsOfType('VM'),
}),
addSubscriptions({
logs: cb =>
subscribeBackupNgLogs(logs =>
cb(
logs &&
groupBy(logs, log =>
log.message === 'restore' ? 'restore' : 'backup'
)
)
cb(logs && filter(logs, log => log.message !== 'restore'))
),
jobs: cb => subscribeBackupNgJobs(jobs => cb(keyBy(jobs, 'id'))),
}),
provideState({
initialState: () => ({
tenPerPageBackup: false,
tenPerPageRestore: false,
}),
effects: {
toggleState,
},
computed: {
backupLogs: (_, { logs, vms }) =>
map(logs.backup, log =>
logs: (_, { logs, vms }) =>
logs &&
logs.map(log =>
log.tasks !== undefined
? {
...log,
@ -358,48 +169,19 @@ export default decorate([
},
}),
injectState,
({ state, effects, logs, jobs, srs, vms }) => (
({ state, jobs }) => (
<Card>
<CardHeader>
<Icon icon='log' /> {_('logTitle')}
<Icon icon='logs' /> {_('logTitle')}
</CardHeader>
<CardBlock>
<h2>
{_('labelBackup')}
<TenPerPage
name='tenPerPageBackup'
handler={effects.toggleState}
value={state.tenPerPageBackup}
/>
</h2>
<NoObjects
collection={logs && state.backupLogs}
columns={LOG_BACKUP_COLUMNS}
collection={state.logs}
columns={COLUMNS}
component={SortedTable}
data-jobs={jobs}
emptyMessage={_('noLogs')}
filters={LOG_FILTERS}
itemsPerPage={state.tenPerPageBackup ? 10 : 3}
/>
<h2>
{_('labelRestore')}
<TenPerPage
name='tenPerPageRestore'
handler={effects.toggleState}
value={state.tenPerPageRestore}
/>
</h2>
<NoObjects
collection={logs && defined(logs.restore, [])}
columns={LOG_RESTORE_COLUMNS}
component={SortedTable}
data-jobs={jobs}
data-srs={srs}
data-vms={vms}
emptyMessage={_('noLogs')}
filters={LOG_FILTERS}
itemsPerPage={state.tenPerPageRestore ? 10 : 3}
rowTransform={ROW_TRANSFORM}
/>
</CardBlock>
</Card>

View File

@ -0,0 +1,181 @@
import _, { FormattedDuration } from 'intl'
import addSubscriptions from 'add-subscriptions'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
import Icon from 'icon'
import NoObjects from 'no-objects'
import React from 'react'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { alert } from 'modal'
import { Card, CardHeader, CardBlock } from 'card'
import { connectStore, formatSize, formatSpeed } from 'utils'
import { createGetObjectsOfType } from 'selectors'
import { filter, keyBy } from 'lodash'
import { get } from '@xen-orchestra/defined'
import { subscribeBackupNgLogs, subscribeBackupNgJobs } from 'xo'
import { Vm, Sr } from 'render-xo-item'
import { STATUS_LABELS, LOG_FILTERS, LogDate } from './utils'
const showRestoreError = ({ currentTarget: { dataset } }) =>
alert(
_('logsFailedRestoreTitle'),
<Copiable data={dataset.error} className='text-danger' tagName='div'>
<Icon icon='alarm' /> {dataset.message}
</Copiable>
)
const COLUMNS = [
{
name: _('logsJobId'),
itemRenderer: ({ data: { jobId } }) => jobId.slice(4, 8),
sortCriteria: 'data.jobId',
},
{
name: _('logsJobName'),
itemRenderer: ({ data: { jobId } }, { jobs }) =>
get(() => jobs[jobId].name),
sortCriteria: ({ data: { jobId } }, { jobs }) =>
get(() => jobs[jobId].name),
},
{
name: _('logsBackupTime'),
itemRenderer: ({ data: { time } }) => <LogDate time={time} />,
sortCriteria: 'data.time',
},
{
name: _('labelVm'),
itemRenderer: ({ id, vm, status }) => (
<div>
{vm !== undefined && <Vm id={vm.id} link newTab />}
{vm === undefined && status === 'success' && (
<span className='text-warning'>{_('logsVmNotFound')}</span>
)}{' '}
<span style={{ fontSize: '0.5em' }} className='text-muted'>
{id}
</span>
</div>
),
sortCriteria: ({ vm }) => vm !== undefined && vm.name_label,
},
{
default: true,
name: _('logsRestoreTime'),
itemRenderer: log => <LogDate time={log.start} />,
sortCriteria: 'start',
sortOrder: 'desc',
},
{
name: _('jobDuration'),
itemRenderer: log =>
log.end !== undefined && (
<FormattedDuration duration={log.end - log.start} />
),
sortCriteria: log => log.end - log.start,
},
{
name: _('labelSr'),
itemRenderer: ({ data: { srId } }) => <Sr id={srId} link newTab />,
sortCriteria: ({ data: { srId } }, { srs }) =>
get(() => srs[srId].name_label),
},
{
name: _('jobStatus'),
itemRenderer: task => {
const { className, label } = STATUS_LABELS[task.status]
return (
<div>
<span className={`tag tag-${className}`}>{_(label)}</span>{' '}
{task.status === 'failure' && (
<Tooltip content={_('logsFailedRestoreError')}>
<a
className='text-danger'
onClick={showRestoreError}
data-message={task.result.message}
data-error={JSON.stringify(task.result)}
style={{ cursor: 'pointer' }}
>
<Icon icon='alarm' />
</a>
</Tooltip>
)}
</div>
)
},
sortCriteria: 'status',
},
{
name: _('labelSize'),
itemRenderer: ({ dataSize }) =>
dataSize !== undefined && formatSize(dataSize),
sortCriteria: 'dataSize',
},
{
name: _('labelSpeed'),
itemRenderer: task => {
const duration = task.end - task.start
return (
task.dataSize !== undefined &&
duration > 0 &&
formatSpeed(task.dataSize, duration)
)
},
sortCriteria: task => {
const duration = task.end - task.start
return (
task.dataSize !== undefined && duration > 0 && task.dataSize / duration
)
},
},
]
const ROW_TRANSFORM = (task, { vms }) => {
let vm, dataSize
if (task.status === 'success') {
const result = task.tasks.find(({ message }) => message === 'transfer')
.result
dataSize = result.size
vm = vms && vms[result.id]
}
return {
...task,
dataSize,
vm,
}
}
export default decorate([
connectStore({
srs: createGetObjectsOfType('SR'),
vms: createGetObjectsOfType('VM'),
}),
addSubscriptions({
logs: cb =>
subscribeBackupNgLogs(logs =>
cb(logs && filter(logs, log => log.message === 'restore'))
),
jobs: cb => subscribeBackupNgJobs(jobs => cb(keyBy(jobs, 'id'))),
}),
({ logs, jobs, srs, vms }) => (
<Card>
<CardHeader>
<Icon icon='logs' /> {_('logTitle')}
</CardHeader>
<CardBlock>
<NoObjects
collection={logs}
columns={COLUMNS}
component={SortedTable}
data-jobs={jobs}
data-srs={srs}
data-vms={vms}
emptyMessage={_('noLogs')}
filters={LOG_FILTERS}
rowTransform={ROW_TRANSFORM}
/>
</CardBlock>
</Card>
),
])

View File

@ -0,0 +1,45 @@
import React from 'react'
import { FormattedDate } from 'react-intl'
export const STATUS_LABELS = {
failure: {
className: 'danger',
label: 'jobFailed',
},
skipped: {
className: 'info',
label: 'jobSkipped',
},
success: {
className: 'success',
label: 'jobSuccess',
},
pending: {
className: 'warning',
label: 'jobStarted',
},
interrupted: {
className: 'danger',
label: 'jobInterrupted',
},
}
export const LOG_FILTERS = {
jobFailed: 'status: failure',
jobInterrupted: 'status: interrupted',
jobSkipped: 'status: skipped',
jobStarted: 'status: pending',
jobSuccess: 'status: success',
}
export const LogDate = ({ time }) => (
<FormattedDate
value={new Date(time)}
month='short'
day='numeric'
year='numeric'
hour='2-digit'
minute='2-digit'
second='2-digit'
/>
)