feat(xo-web/backup-ng/logs): move restore logs to the restore tab (#3802)
Fixes #3772
This commit is contained in:
parent
c55daae734
commit
841a8ed1a5
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
181
packages/xo-web/src/xo-app/logs/restore.js
Normal file
181
packages/xo-web/src/xo-app/logs/restore.js
Normal 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>
|
||||
),
|
||||
])
|
45
packages/xo-web/src/xo-app/logs/utils.js
Normal file
45
packages/xo-web/src/xo-app/logs/utils.js
Normal 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'
|
||||
/>
|
||||
)
|
Loading…
Reference in New Issue
Block a user