From 59985adc5d20d0b5e3e53ea541e2e3f484a610d9 Mon Sep 17 00:00:00 2001 From: badrAZ Date: Tue, 6 Feb 2018 14:39:51 +0100 Subject: [PATCH] feat(backups): improve smart backup feedback (#2320) Fixes #2253 --- package.json | 1 + src/common/intl/messages.js | 5 + src/common/smart-backup-pattern.js | 53 +++++++++ src/xo-app/backup/new/index.js | 165 ++++++++++++++++++++-------- src/xo-app/backup/overview/index.js | 26 ++++- yarn.lock | 6 + 6 files changed, 210 insertions(+), 46 deletions(-) create mode 100644 src/common/smart-backup-pattern.js diff --git a/package.json b/package.json index a61cfa5b8..bc30c8a5e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/intl/messages.js b/src/common/intl/messages.js index ce44dddba..724ed6a15 100644 --- a/src/common/intl/messages.js +++ b/src/common/intl/messages.js @@ -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', diff --git a/src/common/smart-backup-pattern.js b/src/common/smart-backup-pattern.js new file mode 100644 index 000000000..d75c4da08 --- /dev/null +++ b/src/common/smart-backup-pattern.js @@ -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() : '' +} diff --git a/src/xo-app/backup/new/index.js b/src/xo-app/backup/new/index.js index dd5d5401d..d7b9fd8ff 100644 --- a/src/xo-app/backup/new/index.js +++ b/src/xo-app/backup/new/index.js @@ -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 ( + + {_('sampleOfMatchingVms')} + + {nMatchingVms === 0 ? ( +

{_('noMatchingVms')}

+ ) : ( +
+
    + {map(sampleOfMatchingVms, vm => ( +
  • + {renderXoItem(vm)} +
  • + ))} +
+
+ + + {_('allMatchingVms', { + icon: , + nMatchingVms, + })} + + +
+ )} +
+
+ ) + } +} + +// =================================================================== + @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 { {smartBackupMode ? ( - - - {_('vmsToBackup')} - - } - onChange={this.linkState('vmsParam')} - required - schema={SMART_SCHEMA} - uiSchema={SMART_UI_SCHEMA} - value={vms} +
+ + + {_('vmsToBackup')} + + } + onChange={this.linkState('vmsParam')} + required + schema={SMART_SCHEMA} + uiSchema={SMART_UI_SCHEMA} + value={vms} + /> + + - +
) : ( + -
+
- {process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < diff --git a/src/xo-app/backup/overview/index.js b/src/xo-app/backup/overview/index.js index 542e030da..cd8900c6d 100644 --- a/src/xo-app/backup/overview/index.js +++ b/src/xo-app/backup/overview/index.js @@ -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) => (
{!isScheduleUserMissing[schedule.id] && ( @@ -94,6 +95,14 @@ const JOB_COLUMNS = [ + {redirect && ( + + )} { + 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') || diff --git a/yarn.lock b/yarn.lock index 6bd5689ca..30d100a9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"