parent
682d9e88ec
commit
59985adc5d
@ -144,6 +144,7 @@
|
|||||||
"uglify-es": "^3.3.4",
|
"uglify-es": "^3.3.4",
|
||||||
"uncontrollable-input": "^0.1.1",
|
"uncontrollable-input": "^0.1.1",
|
||||||
"url-parse": "^1.2.0",
|
"url-parse": "^1.2.0",
|
||||||
|
"value-matcher": "^0.0.0",
|
||||||
"vinyl": "^2.1.0",
|
"vinyl": "^2.1.0",
|
||||||
"watchify": "^3.7.0",
|
"watchify": "^3.7.0",
|
||||||
"whatwg-fetch": "^2.0.3",
|
"whatwg-fetch": "^2.0.3",
|
||||||
|
@ -187,6 +187,7 @@ const messages = {
|
|||||||
selectAll: 'Select all',
|
selectAll: 'Select all',
|
||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
preview: 'Preview',
|
preview: 'Preview',
|
||||||
|
action: 'Action',
|
||||||
item: 'Item',
|
item: 'Item',
|
||||||
noSelectedValue: 'No selected value',
|
noSelectedValue: 'No selected value',
|
||||||
selectSubjects: 'Choose user(s) and/or group(s)',
|
selectSubjects: 'Choose user(s) and/or group(s)',
|
||||||
@ -299,6 +300,9 @@ const messages = {
|
|||||||
jobOwnerPlaceholder: 'Job owner',
|
jobOwnerPlaceholder: 'Job owner',
|
||||||
jobUserNotFound: "This job's creator no longer exists",
|
jobUserNotFound: "This job's creator no longer exists",
|
||||||
backupUserNotFound: "This backup'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',
|
backupOwner: 'Backup owner',
|
||||||
|
|
||||||
// ------ New backup -----
|
// ------ New backup -----
|
||||||
@ -316,6 +320,7 @@ const messages = {
|
|||||||
editBackupSmartResidentOn: 'Resident on',
|
editBackupSmartResidentOn: 'Resident on',
|
||||||
editBackupSmartPools: 'Pools',
|
editBackupSmartPools: 'Pools',
|
||||||
editBackupSmartTags: 'Tags',
|
editBackupSmartTags: 'Tags',
|
||||||
|
sampleOfMatchingVms: 'Sample of matching Vms',
|
||||||
editBackupSmartTagsTitle: 'VMs Tags',
|
editBackupSmartTagsTitle: 'VMs Tags',
|
||||||
editBackupNot: 'Reverse',
|
editBackupNot: 'Reverse',
|
||||||
editBackupTagTitle: 'Tag',
|
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 GenericInput from 'json-schema-input'
|
||||||
import getEventValue from 'get-event-value'
|
import getEventValue from 'get-event-value'
|
||||||
import Icon from 'icon'
|
import Icon from 'icon'
|
||||||
|
import Link from 'link'
|
||||||
import moment from 'moment-timezone'
|
import moment from 'moment-timezone'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import renderXoItem from 'render-xo-item'
|
||||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||||
|
import Tooltip from 'tooltip'
|
||||||
import uncontrollableInput from 'uncontrollable-input'
|
import uncontrollableInput from 'uncontrollable-input'
|
||||||
import Upgrade from 'xoa-upgrade'
|
import Upgrade from 'xoa-upgrade'
|
||||||
import Wizard, { Section } from 'wizard'
|
import Wizard, { Section } from 'wizard'
|
||||||
import { confirm } from 'modal'
|
import { confirm } from 'modal'
|
||||||
import { connectStore, EMPTY_OBJECT } from 'utils'
|
import { Card, CardBlock, CardHeader } from 'card'
|
||||||
import { Container, Row, Col } from 'grid'
|
import { Container, Row, Col } from 'grid'
|
||||||
|
import { createPredicate } from 'value-matcher'
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
import { generateUiSchema } from 'xo-json-schema-input'
|
import { generateUiSchema } from 'xo-json-schema-input'
|
||||||
import { getUser } from 'selectors'
|
|
||||||
import { SelectSubject } from 'select-objects'
|
import { SelectSubject } from 'select-objects'
|
||||||
|
import { createGetObjectsOfType, getUser } from 'selectors'
|
||||||
|
import { connectStore, EMPTY_OBJECT } from 'utils'
|
||||||
import {
|
import {
|
||||||
|
constructPattern,
|
||||||
|
destructPattern,
|
||||||
|
constructQueryString,
|
||||||
|
} from 'smart-backup-pattern'
|
||||||
|
import {
|
||||||
|
filter,
|
||||||
forEach,
|
forEach,
|
||||||
identity,
|
|
||||||
isArray,
|
isArray,
|
||||||
map,
|
map,
|
||||||
mapValues,
|
mapValues,
|
||||||
noop,
|
noop,
|
||||||
|
pickBy,
|
||||||
startsWith,
|
startsWith,
|
||||||
} from 'lodash'
|
} 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()
|
@uncontrollableInput()
|
||||||
class TimeoutInput extends Component {
|
class TimeoutInput extends Component {
|
||||||
_onChange = event => {
|
_onChange = event => {
|
||||||
@ -313,24 +402,6 @@ const extractId = value => {
|
|||||||
return 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 => {
|
const normalizeMainParams = params => {
|
||||||
if (!('retention' in params)) {
|
if (!('retention' in params)) {
|
||||||
const { depth, ...rest } = 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
|
_getMainParams = () => this.state.mainParams || this._getParams().main
|
||||||
_getVmsParam = () => this.state.vmsParam || this._getParams().vms
|
_getVmsParam = () => this.state.vmsParam || this._getParams().vms
|
||||||
|
|
||||||
@ -456,15 +534,7 @@ export default class New extends Component {
|
|||||||
type: 'map',
|
type: 'map',
|
||||||
collection: {
|
collection: {
|
||||||
type: 'fetchObjects',
|
type: 'fetchObjects',
|
||||||
pattern: {
|
pattern: this._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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
iteratee: {
|
iteratee: {
|
||||||
type: 'extractProperties',
|
type: 'extractProperties',
|
||||||
@ -667,20 +737,25 @@ export default class New extends Component {
|
|||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{smartBackupMode ? (
|
{smartBackupMode ? (
|
||||||
<Upgrade place='newBackup' required={3}>
|
<div>
|
||||||
<GenericInput
|
<Upgrade place='newBackup' required={3}>
|
||||||
label={
|
<GenericInput
|
||||||
<span>
|
label={
|
||||||
<Icon icon='vm' /> {_('vmsToBackup')}
|
<span>
|
||||||
</span>
|
<Icon icon='vm' /> {_('vmsToBackup')}
|
||||||
}
|
</span>
|
||||||
onChange={this.linkState('vmsParam')}
|
}
|
||||||
required
|
onChange={this.linkState('vmsParam')}
|
||||||
schema={SMART_SCHEMA}
|
required
|
||||||
uiSchema={SMART_UI_SCHEMA}
|
schema={SMART_SCHEMA}
|
||||||
value={vms}
|
uiSchema={SMART_UI_SCHEMA}
|
||||||
|
value={vms}
|
||||||
|
/>
|
||||||
|
</Upgrade>
|
||||||
|
<SmartBackupPreview
|
||||||
|
pattern={this._constructPattern(vms)}
|
||||||
/>
|
/>
|
||||||
</Upgrade>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<GenericInput
|
<GenericInput
|
||||||
label={
|
label={
|
||||||
@ -706,12 +781,12 @@ export default class New extends Component {
|
|||||||
onChange={this.linkState('scheduling')}
|
onChange={this.linkState('scheduling')}
|
||||||
value={scheduling}
|
value={scheduling}
|
||||||
/>
|
/>
|
||||||
|
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
||||||
</Section>
|
</Section>
|
||||||
<Section icon='preview' title='preview' summary>
|
<Section title='action' summary>
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
|
||||||
{process.env.XOA_PLAN < 4 &&
|
{process.env.XOA_PLAN < 4 &&
|
||||||
backupInfo &&
|
backupInfo &&
|
||||||
process.env.XOA_PLAN <
|
process.env.XOA_PLAN <
|
||||||
|
@ -11,6 +11,7 @@ import SortedTable from 'sorted-table'
|
|||||||
import StateButton from 'state-button'
|
import StateButton from 'state-button'
|
||||||
import Tooltip from 'tooltip'
|
import Tooltip from 'tooltip'
|
||||||
import { addSubscriptions } from 'utils'
|
import { addSubscriptions } from 'utils'
|
||||||
|
import { constructQueryString } from 'smart-backup-pattern'
|
||||||
import { createSelector } from 'selectors'
|
import { createSelector } from 'selectors'
|
||||||
import { Card, CardHeader, CardBlock } from 'card'
|
import { Card, CardHeader, CardBlock } from 'card'
|
||||||
import { filter, find, forEach, get, map, orderBy } from 'lodash'
|
import { filter, find, forEach, get, map, orderBy } from 'lodash'
|
||||||
@ -80,7 +81,7 @@ const JOB_COLUMNS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: _('jobAction'),
|
name: _('jobAction'),
|
||||||
itemRenderer: ({ schedule }, isScheduleUserMissing) => (
|
itemRenderer: ({ redirect, schedule }, isScheduleUserMissing) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{!isScheduleUserMissing[schedule.id] && (
|
{!isScheduleUserMissing[schedule.id] && (
|
||||||
<Tooltip content={_('backupUserNotFound')}>
|
<Tooltip content={_('backupUserNotFound')}>
|
||||||
@ -94,6 +95,14 @@ const JOB_COLUMNS = [
|
|||||||
<Icon icon='edit' />
|
<Icon icon='edit' />
|
||||||
</Link>
|
</Link>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
{redirect && (
|
||||||
|
<ActionRowButton
|
||||||
|
btnStyle='primary'
|
||||||
|
handler={redirect}
|
||||||
|
icon='preview'
|
||||||
|
tooltip={_('redirectToMatchingVms')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ActionRowButton
|
<ActionRowButton
|
||||||
icon='delete'
|
icon='delete'
|
||||||
btnStyle='danger'
|
btnStyle='danger'
|
||||||
@ -120,6 +129,10 @@ const JOB_COLUMNS = [
|
|||||||
users: subscribeUsers,
|
users: subscribeUsers,
|
||||||
})
|
})
|
||||||
export default class Overview extends Component {
|
export default class Overview extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
router: React.PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
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(
|
_getScheduleCollection = createSelector(
|
||||||
() => this.state.schedules,
|
() => this.state.schedules,
|
||||||
() => this.state.scheduleTable,
|
() => this.state.scheduleTable,
|
||||||
@ -178,10 +198,14 @@ export default class Overview extends Component {
|
|||||||
return map(schedules, schedule => {
|
return map(schedules, schedule => {
|
||||||
const job = jobs[schedule.job]
|
const job = jobs[schedule.job]
|
||||||
const { items } = job.paramsVector
|
const { items } = job.paramsVector
|
||||||
|
const pattern = get(items, '[1].collection.pattern')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
jobLabel: jobKeyToLabel[job.key] || _('unknownSchedule'),
|
jobLabel: jobKeyToLabel[job.key] || _('unknownSchedule'),
|
||||||
|
redirect:
|
||||||
|
pattern !== undefined &&
|
||||||
|
(() => this._redirectToMatchingVms(pattern)),
|
||||||
// Old versions of XenOrchestra use items[0]
|
// Old versions of XenOrchestra use items[0]
|
||||||
scheduleTag:
|
scheduleTag:
|
||||||
get(items, '[0].values[0].tag') ||
|
get(items, '[0].values[0].tag') ||
|
||||||
|
@ -8695,6 +8695,12 @@ validate-npm-package-license@^3.0.1:
|
|||||||
spdx-correct "~1.0.0"
|
spdx-correct "~1.0.0"
|
||||||
spdx-expression-parse "~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:
|
value-or-function@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
|
resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
|
||||||
|
Loading…
Reference in New Issue
Block a user