parent
682d9e88ec
commit
59985adc5d
@ -144,6 +144,7 @@
|
||||
"uglify-es": "^3.3.4",
|
||||
"uncontrollable-input": "^0.1.1",
|
||||
"url-parse": "^1.2.0",
|
||||
"value-matcher": "^0.0.0",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
|
@ -187,6 +187,7 @@ const messages = {
|
||||
selectAll: 'Select all',
|
||||
remove: 'Remove',
|
||||
preview: 'Preview',
|
||||
action: 'Action',
|
||||
item: 'Item',
|
||||
noSelectedValue: 'No selected value',
|
||||
selectSubjects: 'Choose user(s) and/or group(s)',
|
||||
@ -299,6 +300,9 @@ const messages = {
|
||||
jobOwnerPlaceholder: 'Job owner',
|
||||
jobUserNotFound: "This job's creator no longer exists",
|
||||
backupUserNotFound: "This backup's creator no longer exists",
|
||||
redirectToMatchingVms: 'Click here to see the matching VMs',
|
||||
noMatchingVms: 'There are no matching VMs!',
|
||||
allMatchingVms: '{icon} See the matching VMs ({nMatchingVms, number})',
|
||||
backupOwner: 'Backup owner',
|
||||
|
||||
// ------ New backup -----
|
||||
@ -316,6 +320,7 @@ const messages = {
|
||||
editBackupSmartResidentOn: 'Resident on',
|
||||
editBackupSmartPools: 'Pools',
|
||||
editBackupSmartTags: 'Tags',
|
||||
sampleOfMatchingVms: 'Sample of matching Vms',
|
||||
editBackupSmartTagsTitle: 'VMs Tags',
|
||||
editBackupNot: 'Reverse',
|
||||
editBackupTagTitle: 'Tag',
|
||||
|
53
src/common/smart-backup-pattern.js
Normal file
53
src/common/smart-backup-pattern.js
Normal file
@ -0,0 +1,53 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import { flatten, identity, map } from 'lodash'
|
||||
|
||||
import { EMPTY_OBJECT } from './utils'
|
||||
|
||||
export const destructPattern = (pattern, valueTransform = identity) =>
|
||||
pattern && {
|
||||
not: !!pattern.__not,
|
||||
values: valueTransform((pattern.__not || pattern).__or),
|
||||
}
|
||||
|
||||
export const constructPattern = (
|
||||
{ not, values } = EMPTY_OBJECT,
|
||||
valueTransform = identity
|
||||
) => {
|
||||
if (values == null || !values.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const pattern = { __or: valueTransform(values) }
|
||||
return not ? { __not: pattern } : pattern
|
||||
}
|
||||
|
||||
const parsePattern = pattern => {
|
||||
const patternValues = flatten(
|
||||
pattern.__not !== undefined ? pattern.__not.__or : pattern.__or
|
||||
)
|
||||
|
||||
const queryString = new CM.Or(
|
||||
map(patternValues, array => new CM.String(array))
|
||||
)
|
||||
return pattern.__not !== undefined ? CM.Not(queryString) : queryString
|
||||
}
|
||||
|
||||
export const constructQueryString = pattern => {
|
||||
const powerState = pattern.power_state
|
||||
const pool = pattern.$pool
|
||||
const tags = pattern.tags
|
||||
|
||||
const filter = []
|
||||
|
||||
if (powerState !== undefined) {
|
||||
filter.push(new CM.Property('power_state', new CM.String(powerState)))
|
||||
}
|
||||
if (pool !== undefined) {
|
||||
filter.push(new CM.Property('$pool', parsePattern(pool)))
|
||||
}
|
||||
if (tags !== undefined) {
|
||||
filter.push(new CM.Property('tags', parsePattern(tags)))
|
||||
}
|
||||
|
||||
return filter.length !== 0 ? new CM.And(filter).toString() : ''
|
||||
}
|
@ -5,26 +5,38 @@ import Component from 'base-component'
|
||||
import GenericInput from 'json-schema-input'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import moment from 'moment-timezone'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
import Tooltip from 'tooltip'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { confirm } from 'modal'
|
||||
import { connectStore, EMPTY_OBJECT } from 'utils'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createPredicate } from 'value-matcher'
|
||||
import { createSelector } from 'reselect'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { getUser } from 'selectors'
|
||||
import { SelectSubject } from 'select-objects'
|
||||
import { createGetObjectsOfType, getUser } from 'selectors'
|
||||
import { connectStore, EMPTY_OBJECT } from 'utils'
|
||||
import {
|
||||
constructPattern,
|
||||
destructPattern,
|
||||
constructQueryString,
|
||||
} from 'smart-backup-pattern'
|
||||
import {
|
||||
filter,
|
||||
forEach,
|
||||
identity,
|
||||
isArray,
|
||||
map,
|
||||
mapValues,
|
||||
noop,
|
||||
pickBy,
|
||||
startsWith,
|
||||
} from 'lodash'
|
||||
|
||||
@ -274,6 +286,83 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const SAMPLE_SIZE_OF_MATCHING_VMS = 3
|
||||
|
||||
@connectStore({
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
})
|
||||
class SmartBackupPreview extends Component {
|
||||
static propTypes = {
|
||||
pattern: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
_getMatchingVms = createSelector(
|
||||
() => this.props.vms,
|
||||
createSelector(
|
||||
() => this.props.pattern,
|
||||
pattern => createPredicate(pickBy(pattern, val => val != null))
|
||||
),
|
||||
(vms, predicate) => filter(vms, predicate)
|
||||
)
|
||||
|
||||
_getSampleOfMatchingVms = createSelector(this._getMatchingVms, vms =>
|
||||
vms.slice(0, SAMPLE_SIZE_OF_MATCHING_VMS)
|
||||
)
|
||||
|
||||
_getQueryString = createSelector(
|
||||
() => this.props.pattern,
|
||||
constructQueryString
|
||||
)
|
||||
|
||||
render () {
|
||||
const nMatchingVms = this._getMatchingVms().length
|
||||
const sampleOfMatchingVms = this._getSampleOfMatchingVms()
|
||||
const queryString = this._getQueryString()
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>{_('sampleOfMatchingVms')}</CardHeader>
|
||||
<CardBlock>
|
||||
{nMatchingVms === 0 ? (
|
||||
<p className='text-xs-center'>{_('noMatchingVms')}</p>
|
||||
) : (
|
||||
<div>
|
||||
<ul className='list-group'>
|
||||
{map(sampleOfMatchingVms, vm => (
|
||||
<li className='list-group-item' key={vm.id}>
|
||||
{renderXoItem(vm)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<br />
|
||||
<Tooltip content={_('redirectToMatchingVms')}>
|
||||
<Link
|
||||
className='pull-right'
|
||||
target='_blank'
|
||||
to={{
|
||||
pathname: '/home',
|
||||
query: {
|
||||
t: 'VM',
|
||||
s: queryString,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{_('allMatchingVms', {
|
||||
icon: <Icon icon='preview' />,
|
||||
nMatchingVms,
|
||||
})}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@uncontrollableInput()
|
||||
class TimeoutInput extends Component {
|
||||
_onChange = event => {
|
||||
@ -313,24 +402,6 @@ const extractId = value => {
|
||||
return value
|
||||
}
|
||||
|
||||
const destructPattern = (pattern, valueTransform = identity) =>
|
||||
pattern && {
|
||||
not: !!pattern.__not,
|
||||
values: valueTransform((pattern.__not || pattern).__or),
|
||||
}
|
||||
|
||||
const constructPattern = (
|
||||
{ not, values } = EMPTY_OBJECT,
|
||||
valueTransform = identity
|
||||
) => {
|
||||
if (values == null || !values.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const pattern = { __or: valueTransform(values) }
|
||||
return not ? { __not: pattern } : pattern
|
||||
}
|
||||
|
||||
const normalizeMainParams = params => {
|
||||
if (!('retention' in params)) {
|
||||
const { depth, ...rest } = params
|
||||
@ -399,6 +470,13 @@ export default class New extends Component {
|
||||
}
|
||||
)
|
||||
|
||||
_constructPattern = vms => ({
|
||||
$pool: constructPattern(vms.$pool),
|
||||
power_state: vms.power_state === 'All' ? undefined : vms.power_state,
|
||||
tags: constructPattern(vms.tags, tags => map(tags, tag => [tag])),
|
||||
type: 'VM',
|
||||
})
|
||||
|
||||
_getMainParams = () => this.state.mainParams || this._getParams().main
|
||||
_getVmsParam = () => this.state.vmsParam || this._getParams().vms
|
||||
|
||||
@ -456,15 +534,7 @@ export default class New extends Component {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: constructPattern(vms.$pool),
|
||||
power_state:
|
||||
vms.power_state === 'All' ? undefined : vms.power_state,
|
||||
tags: constructPattern(vms.tags, tags =>
|
||||
map(tags, tag => [tag])
|
||||
),
|
||||
type: 'VM',
|
||||
},
|
||||
pattern: this._constructPattern(vms),
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
@ -667,20 +737,25 @@ export default class New extends Component {
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode ? (
|
||||
<Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
label={
|
||||
<span>
|
||||
<Icon icon='vm' /> {_('vmsToBackup')}
|
||||
</span>
|
||||
}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
<div>
|
||||
<Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
label={
|
||||
<span>
|
||||
<Icon icon='vm' /> {_('vmsToBackup')}
|
||||
</span>
|
||||
}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
</Upgrade>
|
||||
<SmartBackupPreview
|
||||
pattern={this._constructPattern(vms)}
|
||||
/>
|
||||
</Upgrade>
|
||||
</div>
|
||||
) : (
|
||||
<GenericInput
|
||||
label={
|
||||
@ -706,12 +781,12 @@ export default class New extends Component {
|
||||
onChange={this.linkState('scheduling')}
|
||||
value={scheduling}
|
||||
/>
|
||||
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
||||
</Section>
|
||||
<Section icon='preview' title='preview' summary>
|
||||
<Section title='action' summary>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 &&
|
||||
backupInfo &&
|
||||
process.env.XOA_PLAN <
|
||||
|
@ -11,6 +11,7 @@ import SortedTable from 'sorted-table'
|
||||
import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { constructQueryString } from 'smart-backup-pattern'
|
||||
import { createSelector } from 'selectors'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { filter, find, forEach, get, map, orderBy } from 'lodash'
|
||||
@ -80,7 +81,7 @@ const JOB_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('jobAction'),
|
||||
itemRenderer: ({ schedule }, isScheduleUserMissing) => (
|
||||
itemRenderer: ({ redirect, schedule }, isScheduleUserMissing) => (
|
||||
<fieldset>
|
||||
{!isScheduleUserMissing[schedule.id] && (
|
||||
<Tooltip content={_('backupUserNotFound')}>
|
||||
@ -94,6 +95,14 @@ const JOB_COLUMNS = [
|
||||
<Icon icon='edit' />
|
||||
</Link>
|
||||
<ButtonGroup>
|
||||
{redirect && (
|
||||
<ActionRowButton
|
||||
btnStyle='primary'
|
||||
handler={redirect}
|
||||
icon='preview'
|
||||
tooltip={_('redirectToMatchingVms')}
|
||||
/>
|
||||
)}
|
||||
<ActionRowButton
|
||||
icon='delete'
|
||||
btnStyle='danger'
|
||||
@ -120,6 +129,10 @@ const JOB_COLUMNS = [
|
||||
users: subscribeUsers,
|
||||
})
|
||||
export default class Overview extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
@ -166,6 +179,13 @@ export default class Overview extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_redirectToMatchingVms = pattern => {
|
||||
this.context.router.push({
|
||||
pathname: '/home',
|
||||
query: { t: 'VM', s: constructQueryString(pattern) },
|
||||
})
|
||||
}
|
||||
|
||||
_getScheduleCollection = createSelector(
|
||||
() => this.state.schedules,
|
||||
() => this.state.scheduleTable,
|
||||
@ -178,10 +198,14 @@ export default class Overview extends Component {
|
||||
return map(schedules, schedule => {
|
||||
const job = jobs[schedule.job]
|
||||
const { items } = job.paramsVector
|
||||
const pattern = get(items, '[1].collection.pattern')
|
||||
|
||||
return {
|
||||
jobId: job.id,
|
||||
jobLabel: jobKeyToLabel[job.key] || _('unknownSchedule'),
|
||||
redirect:
|
||||
pattern !== undefined &&
|
||||
(() => this._redirectToMatchingVms(pattern)),
|
||||
// Old versions of XenOrchestra use items[0]
|
||||
scheduleTag:
|
||||
get(items, '[0].values[0].tag') ||
|
||||
|
@ -8695,6 +8695,12 @@ validate-npm-package-license@^3.0.1:
|
||||
spdx-correct "~1.0.0"
|
||||
spdx-expression-parse "~1.0.0"
|
||||
|
||||
value-matcher@^0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/value-matcher/-/value-matcher-0.0.0.tgz#c0caf87dc3998a68ea56b31fd1916adefe39f7be"
|
||||
dependencies:
|
||||
"@babel/polyfill" "^7.0.0-beta.36"
|
||||
|
||||
value-or-function@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
|
||||
|
Loading…
Reference in New Issue
Block a user