Files
xen-orchestra/packages/xo-web/src/xo-app/backup-ng/index.js

438 lines
12 KiB
JavaScript

import _ from 'intl'
import ActionButton from 'action-button'
import addSubscriptions from 'add-subscriptions'
import Button from 'button'
import ButtonLink from 'button-link'
import Copiable from 'copiable'
import CopyToClipboard from 'react-copy-to-clipboard'
import decorate from 'apply-decorators'
import Icon from 'icon'
import PropTypes from 'prop-types'
import React from 'react'
import SortedTable from 'sorted-table'
import StateButton from 'state-button'
import Tooltip from 'tooltip'
import { Card, CardHeader, CardBlock } from 'card'
import { confirm } from 'modal'
import { connectStore, routes } from 'utils'
import { constructQueryString } from 'smart-backup'
import { Container, Row, Col } from 'grid'
import { createGetLoneSnapshots, createSelector } from 'selectors'
import { get } from '@xen-orchestra/defined'
import { isEmpty, map, groupBy, some } from 'lodash'
import { NavLink, NavTabs } from 'nav'
import {
cancelJob,
deleteBackupJobs,
disableSchedule,
enableSchedule,
runBackupNgJob,
runMetadataBackupJob,
subscribeBackupNgJobs,
subscribeBackupNgLogs,
subscribeMetadataBackupJobs,
subscribeSchedules,
} from 'xo'
import LogsTable, { LogStatus } from '../logs/backup-ng'
import Page from '../page'
import Edit from './edit'
import FileRestore from './file-restore'
import getSettingsWithNonDefaultValue from './_getSettingsWithNonDefaultValue'
import Health from './health'
import NewVmBackup, { NewMetadataBackup } from './new'
import Restore, { RestoreMetadata } from './restore'
import { destructPattern } from './utils'
const Ul = props => <ul {...props} style={{ listStyleType: 'none' }} />
const Li = props => (
<li
{...props}
style={{
whiteSpace: 'nowrap',
}}
/>
)
const _runBackupJob = ({ id, name, schedule, type }) =>
confirm({
title: _('runJob'),
body: _('runBackupNgJobConfirm', {
id: id.slice(0, 5),
name: <strong>{name}</strong>,
}),
}).then(() =>
type === 'backup'
? runBackupNgJob({ id, schedule })
: runMetadataBackupJob({ id, schedule })
)
const _deleteBackupJobs = items => {
const { backup: backupIds, metadataBackup: metadataBackupIds } = groupBy(
items,
'type'
)
return deleteBackupJobs({ backupIds, metadataBackupIds })
}
const SchedulePreviewBody = decorate([
addSubscriptions(({ schedule }) => ({
lastRunLog: cb =>
subscribeBackupNgLogs(logs => {
let lastRunLog
for (const runId in logs) {
const log = logs[runId]
if (
log.scheduleId === schedule.id &&
(lastRunLog === undefined || lastRunLog.start < log.start)
) {
lastRunLog = log
}
}
cb(lastRunLog)
}),
})),
({ job, schedule, lastRunLog }) => (
<Ul>
<Li>
{schedule.name
? _.keyValue(_('scheduleName'), schedule.name)
: _.keyValue(_('scheduleCron'), schedule.cron)}{' '}
<Tooltip content={_('scheduleCopyId', { id: schedule.id.slice(4, 8) })}>
<CopyToClipboard text={schedule.id}>
<Button size='small'>
<Icon icon='clipboard' />
</Button>
</CopyToClipboard>
</Tooltip>
</Li>
<Li>
<StateButton
disabledLabel={_('stateDisabled')}
disabledHandler={enableSchedule}
disabledTooltip={_('logIndicationToEnable')}
enabledLabel={_('stateEnabled')}
enabledHandler={disableSchedule}
enabledTooltip={_('logIndicationToDisable')}
handlerParam={schedule.id}
state={schedule.enabled}
style={{ marginRight: '0.5em' }}
/>
{job.runId !== undefined ? (
<ActionButton
btnStyle='danger'
handler={cancelJob}
handlerParam={job}
icon='cancel'
key='cancel'
size='small'
tooltip={_('formCancel')}
/>
) : (
<ActionButton
btnStyle='primary'
data-id={job.id}
data-name={job.name}
data-schedule={schedule.id}
data-type={job.type}
handler={_runBackupJob}
icon='run-schedule'
key='run'
size='small'
/>
)}{' '}
{lastRunLog !== undefined && (
<LogStatus log={lastRunLog} tooltip={_('scheduleLastRun')} />
)}
</Li>
</Ul>
),
])
const MODES = [
{
label: 'rollingSnapshot',
test: job =>
some(job.settings, ({ snapshotRetention }) => snapshotRetention > 0),
},
{
label: 'backup',
test: job =>
job.mode === 'full' && !isEmpty(get(() => destructPattern(job.remotes))),
},
{
label: 'deltaBackup',
test: job =>
job.mode === 'delta' && !isEmpty(get(() => destructPattern(job.remotes))),
},
{
label: 'continuousReplication',
test: job =>
job.mode === 'delta' && !isEmpty(get(() => destructPattern(job.srs))),
},
{
label: 'disasterRecovery',
test: job =>
job.mode === 'full' && !isEmpty(get(() => destructPattern(job.srs))),
},
{
label: 'poolMetadata',
test: job => !isEmpty(destructPattern(job.pools)),
},
{
label: 'xoConfig',
test: job => job.xoMetadata,
},
]
@addSubscriptions({
jobs: subscribeBackupNgJobs,
metadataJobs: subscribeMetadataBackupJobs,
schedulesByJob: cb =>
subscribeSchedules(schedules => {
cb(groupBy(schedules, 'jobId'))
}),
})
class JobsTable extends React.Component {
static contextTypes = {
router: PropTypes.object,
}
static tableProps = {
actions: [
{
handler: _deleteBackupJobs,
label: _('deleteBackupSchedule'),
icon: 'delete',
level: 'danger',
},
],
columns: [
{
itemRenderer: ({ id }) => (
<Copiable data={id} tagName='p'>
{id.slice(4, 8)}
</Copiable>
),
name: _('jobId'),
},
{
valuePath: 'name',
name: _('jobName'),
default: true,
},
{
itemRenderer: job => (
<Ul>
{MODES.filter(({ test }) => test(job)).map(({ label }) => (
<Li key={label}>{_(label)}</Li>
))}
</Ul>
),
sortCriteria: 'mode',
name: _('jobModes'),
},
{
itemRenderer: (job, { schedulesByJob }) =>
map(get(() => schedulesByJob[job.id]), schedule => (
<SchedulePreviewBody
job={job}
key={schedule.id}
schedule={schedule}
/>
)),
name: _('jobSchedules'),
},
{
itemRenderer: job => {
const {
compression,
concurrency,
fullInterval,
offlineSnapshot,
reportWhen,
timeout,
} = getSettingsWithNonDefaultValue(job.mode, {
compression: job.compression,
...job.settings[''],
})
return (
<Ul>
{reportWhen !== undefined && (
<Li>{_.keyValue(_('reportWhen'), reportWhen)}</Li>
)}
{concurrency !== undefined && (
<Li>{_.keyValue(_('concurrency'), concurrency)}</Li>
)}
{timeout !== undefined && (
<Li>{_.keyValue(_('timeout'), timeout / 3600e3)} hours</Li>
)}
{fullInterval !== undefined && (
<Li>{_.keyValue(_('fullBackupInterval'), fullInterval)}</Li>
)}
{offlineSnapshot !== undefined && (
<Li>
{_.keyValue(
_('offlineSnapshot'),
_(offlineSnapshot ? 'stateEnabled' : 'stateDisabled')
)}
</Li>
)}
{compression !== undefined && (
<Li>
{_.keyValue(
_('compression'),
compression === 'native' ? 'GZIP' : compression
)}
</Li>
)}
</Ul>
)
},
name: _('formNotes'),
},
],
individualActions: [
{
handler: (job, { goTo }) =>
goTo({
pathname: '/home',
query: { t: 'VM', s: constructQueryString(job.vms) },
}),
disabled: job => job.type !== 'backup',
label: _('redirectToMatchingVms'),
icon: 'preview',
},
{
handler: (job, { goTo }) => goTo(`/backup-ng/${job.id}/edit`),
label: _('formEdit'),
icon: 'edit',
level: 'primary',
},
],
}
_goTo = path => {
this.context.router.push(path)
}
_getCollection = createSelector(
() => this.props.jobs,
() => this.props.metadataJobs,
(jobs = [], metadataJobs = []) => [...jobs, ...metadataJobs]
)
render() {
return (
<SortedTable
{...JobsTable.tableProps}
collection={this._getCollection()}
data-goTo={this._goTo}
data-schedulesByJob={this.props.schedulesByJob}
/>
)
}
}
const Overview = () => (
<div>
<Card>
<CardHeader>
<Icon icon='backup' /> {_('backupJobs')}
</CardHeader>
<CardBlock>
<JobsTable />
</CardBlock>
</Card>
<LogsTable />
</div>
)
const HealthNavTab = decorate([
addSubscriptions({
// used by createGetLoneSnapshots
schedules: subscribeSchedules,
}),
connectStore({
nLoneSnapshots: createGetLoneSnapshots.count(),
}),
({ nLoneSnapshots }) => (
<NavLink to='/backup-ng/health'>
<Icon icon='menu-dashboard-health' /> {_('overviewHealthDashboardPage')}{' '}
{nLoneSnapshots > 0 && (
<Tooltip content={_('loneSnapshotsMessages', { nLoneSnapshots })}>
<span className='tag tag-pill tag-warning'>{nLoneSnapshots}</span>
</Tooltip>
)}
</NavLink>
),
])
const HEADER = (
<Container>
<Row>
<Col mediumSize={3}>
<h2>
<Icon icon='backup' /> {_('backupPage')}
</h2>
</Col>
<Col mediumSize={9}>
<NavTabs className='pull-right'>
<NavLink exact to='/backup-ng/overview'>
<Icon icon='menu-backup-overview' /> {_('backupOverviewPage')}
</NavLink>
<NavLink to='/backup-ng/new'>
<Icon icon='menu-backup-new' /> {_('backupNewPage')}
</NavLink>
<NavLink to='/backup-ng/restore'>
<Icon icon='menu-backup-restore' /> {_('backupRestorePage')}
</NavLink>
<NavLink to='/backup-ng/file-restore'>
<Icon icon='menu-backup-file-restore' />{' '}
{_('backupFileRestorePage')}
</NavLink>
<HealthNavTab />
</NavTabs>
</Col>
</Row>
</Container>
)
const ChooseBackupType = () => (
<Container>
<Row>
<Col>
<Card>
<CardHeader>{_('backupType')}</CardHeader>
<CardBlock className='text-md-center'>
<ButtonLink to='backup-ng/new/vms'>
<Icon icon='backup' /> {_('backupVms')}
</ButtonLink>{' '}
<ButtonLink to='backup-ng/new/metadata'>
<Icon icon='database' /> {_('backupMetadata')}
</ButtonLink>
</CardBlock>
</Card>
</Col>
</Row>
</Container>
)
export default routes('overview', {
':id/edit': Edit,
new: ChooseBackupType,
'new/vms': NewVmBackup,
'new/metadata': NewMetadataBackup,
overview: Overview,
restore: Restore,
'restore/metadata': RestoreMetadata,
'file-restore': FileRestore,
health: Health,
})(({ children }) => (
<Page header={HEADER} title='backupPage' formatTitle>
{children}
</Page>
))