From 32dd16114ef6fb6a4c4ba384ae97c0d3014ba7c1 Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Mon, 29 Jan 2024 15:56:43 +0100 Subject: [PATCH] fix(xo-web/backup): smart mode preview should ignore `xo:no-bak` tags (#7331) Introduced by 87a9fbe23 Fixes https://xcp-ng.org/forum/post/69797 --- CHANGELOG.unreleased.md | 1 + .../src/common/construct-query-string.js | 50 ---------------- .../xo-web/src/common/smart-backup/preview.js | 23 ++++--- .../src/common/smartModeToComplexMatcher.js | 60 +++++++++++++++++++ packages/xo-web/src/common/zstd-checker.js | 6 +- .../src/xo-app/backup/overview/tab-jobs.js | 4 +- 6 files changed, 81 insertions(+), 63 deletions(-) delete mode 100644 packages/xo-web/src/common/construct-query-string.js create mode 100644 packages/xo-web/src/common/smartModeToComplexMatcher.js diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 60b62bb85..72bbb766a 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -42,6 +42,7 @@ - [Backup/Restore] Don't count memory as a key (i.e. complete) disk [Forum#8212](https://xcp-ng.org/forum/post/69591) (PR [#7315](https://github.com/vatesfr/xen-orchestra/pull/7315)) - [Pool/patches] Disable Rolling Pool Update button if host is alone in its pool [#6415](https://github.com/vatesfr/xen-orchestra/issues/6415) (PR [#7286](https://github.com/vatesfr/xen-orchestra/pull/7286)) - [PIF] Fix IPv4 reconfiguration only worked when the IPv4 mode was updated (PR [#7324](https://github.com/vatesfr/xen-orchestra/pull/7324)) +- [Backup/Smart mode] Make preview correctly ignoring `xo:no-bak` tags [Forum#69797](https://xcp-ng.org/forum/post/69797) (PR [#7331](https://github.com/vatesfr/xen-orchestra/pull/7331)) ### Packages to release diff --git a/packages/xo-web/src/common/construct-query-string.js b/packages/xo-web/src/common/construct-query-string.js deleted file mode 100644 index 5902cc83e..000000000 --- a/packages/xo-web/src/common/construct-query-string.js +++ /dev/null @@ -1,50 +0,0 @@ -import * as CM from 'complex-matcher' -import escapeRegExp from 'lodash/escapeRegExp.js' - -const valueToComplexMatcher = pattern => { - if (typeof pattern === 'string') { - return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i') - } - - if (Array.isArray(pattern)) { - return new CM.And(pattern.map(valueToComplexMatcher)) - } - - if (pattern !== null && typeof pattern === 'object') { - const keys = Object.keys(pattern) - const { length } = keys - - if (length === 1) { - const [key] = keys - if (key === '__and') { - return new CM.And(pattern.__and.map(valueToComplexMatcher)) - } - if (key === '__or') { - return new CM.Or(pattern.__or.map(valueToComplexMatcher)) - } - if (key === '__not') { - return new CM.Not(valueToComplexMatcher(pattern.__not)) - } - } - - const children = [] - Object.keys(pattern).forEach(property => { - const subpattern = pattern[property] - if (subpattern !== undefined) { - children.push(new CM.Property(property, valueToComplexMatcher(subpattern))) - } - }) - return children.length === 0 ? new CM.Null() : new CM.And(children) - } - - throw new Error('could not transform this pattern') -} - -export default pattern => { - try { - return valueToComplexMatcher(pattern).toString() - } catch (error) { - console.warn('constructQueryString', pattern, error) - return '' - } -} diff --git a/packages/xo-web/src/common/smart-backup/preview.js b/packages/xo-web/src/common/smart-backup/preview.js index 10511fd0f..5ac140c86 100644 --- a/packages/xo-web/src/common/smart-backup/preview.js +++ b/packages/xo-web/src/common/smart-backup/preview.js @@ -1,17 +1,16 @@ import _ from 'intl' import PropTypes from 'prop-types' import React from 'react' -import { createPredicate } from 'value-matcher' import { createSelector } from 'reselect' -import { filter, map, pickBy } from 'lodash' +import { filter, map } from 'lodash' import Component from './../base-component' -import constructQueryString from '../construct-query-string' import Icon from './../icon' import Link from './../link' import renderXoItem from './../render-xo-item' import Tooltip from './../tooltip' import { Card, CardBlock, CardHeader } from './../card' +import { smartModeToComplexMatcher } from '../smartModeToComplexMatcher' const SAMPLE_SIZE_OF_MATCHING_VMS = 3 @@ -21,18 +20,26 @@ export default class SmartBackupPreview extends Component { vms: PropTypes.object.isRequired, } + // user pattern completed with support for `xo:no-bak` tag automatically + // ignored by xo-server + _getComplexMatcher = createSelector(() => this.props.pattern, smartModeToComplexMatcher) + _getMatchingVms = createSelector( () => this.props.vms, - createSelector( - () => this.props.pattern, - pattern => createPredicate(pickBy(pattern, val => val != null)) - ), + createSelector(this._getComplexMatcher, cm => cm.createPredicate()), (vms, predicate) => filter(vms, predicate) ) _getSampleOfMatchingVms = createSelector(this._getMatchingVms, vms => vms.slice(0, SAMPLE_SIZE_OF_MATCHING_VMS)) - _getQueryString = createSelector(() => this.props.pattern, constructQueryString) + _getQueryString = createSelector(this._getComplexMatcher, cm => { + try { + return cm.toString() + } catch (error) { + console.error(error) + return '' + } + }) render() { const nMatchingVms = this._getMatchingVms().length diff --git a/packages/xo-web/src/common/smartModeToComplexMatcher.js b/packages/xo-web/src/common/smartModeToComplexMatcher.js new file mode 100644 index 000000000..4ad98259e --- /dev/null +++ b/packages/xo-web/src/common/smartModeToComplexMatcher.js @@ -0,0 +1,60 @@ +import * as CM from 'complex-matcher' +import escapeRegExp from 'lodash/escapeRegExp.js' + +// compile a value-matcher like pattern (plus support for regexps) to a +// complex-matcher pattern +const pseudoValueToComplexMatcher = pattern => { + if (typeof pattern === 'string') { + return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i') + } + + if (Array.isArray(pattern)) { + return new CM.And(pattern.map(pseudoValueToComplexMatcher)) + } + + if (pattern instanceof RegExp) { + return new CM.RegExpNode(pattern) + } + + if (pattern !== null && typeof pattern === 'object') { + const keys = Object.keys(pattern) + const { length } = keys + + if (length === 1) { + const [key] = keys + if (key === '__and') { + return new CM.And(pattern.__and.map(pseudoValueToComplexMatcher)) + } + if (key === '__or') { + return new CM.Or(pattern.__or.map(pseudoValueToComplexMatcher)) + } + if (key === '__not') { + return new CM.Not(pseudoValueToComplexMatcher(pattern.__not)) + } + } + + const children = [] + Object.keys(pattern).forEach(property => { + const subpattern = pattern[property] + if (subpattern !== undefined) { + children.push(new CM.Property(property, pseudoValueToComplexMatcher(subpattern))) + } + }) + return children.length === 0 ? new CM.Null() : new CM.And(children) + } + + throw new Error('could not transform this pattern') +} + +export const smartModeToComplexMatcher = pattern => { + // don't mutate param + pattern = JSON.parse(JSON.stringify(pattern)) + + // if the pattern does not match expected entries, simply don't change it + const { tags } = pattern + if (tags !== undefined) { + ;(tags.__not ?? tags.__and?.[1]?.__not)?.__or?.push(/^xo:no-bak(?:=.*)?$/) + } + + return pseudoValueToComplexMatcher(pattern) +} diff --git a/packages/xo-web/src/common/zstd-checker.js b/packages/xo-web/src/common/zstd-checker.js index 50b2afd41..3a72b3277 100644 --- a/packages/xo-web/src/common/zstd-checker.js +++ b/packages/xo-web/src/common/zstd-checker.js @@ -3,12 +3,12 @@ import React from 'react' import _ from './intl' import Component from './base-component' -import constructQueryString from './construct-query-string' import Icon from './icon' import Link from './link' import Tooltip from './tooltip' import { connectStore } from './utils' import { createCollectionWrapper, createGetObjectsOfType, createSelector } from './selectors' +import { smartModeToComplexMatcher } from './smartModeToComplexMatcher' @connectStore({ containers: createSelector(createGetObjectsOfType('pool'), createGetObjectsOfType('host'), (pools, hosts) => ({ @@ -41,11 +41,11 @@ export default class ZstdChecker extends Component { pathname: '/home', query: { t: 'VM', - s: constructQueryString({ + s: smartModeToComplexMatcher({ id: { __or: vms, }, - }), + }).toString(), }, })) diff --git a/packages/xo-web/src/xo-app/backup/overview/tab-jobs.js b/packages/xo-web/src/xo-app/backup/overview/tab-jobs.js index d82463e2f..948d038fc 100644 --- a/packages/xo-web/src/xo-app/backup/overview/tab-jobs.js +++ b/packages/xo-web/src/xo-app/backup/overview/tab-jobs.js @@ -3,7 +3,6 @@ import _ from 'intl' import ActionButton from 'action-button' import addSubscriptions from 'add-subscriptions' import Button from 'button' -import constructQueryString from 'construct-query-string' import Copiable from 'copiable' import CopyToClipboard from 'react-copy-to-clipboard' import decorate from 'apply-decorators' @@ -21,6 +20,7 @@ import { get } from '@xen-orchestra/defined' import { groupBy, isEmpty, map, some } from 'lodash' import { injectState, provideState } from 'reaclette' import { Proxy } from 'render-xo-item' +import { smartModeToComplexMatcher } from 'smartModeToComplexMatcher' import { withRouter } from 'react-router' import { cancelJob, @@ -360,7 +360,7 @@ class JobsTable extends React.Component { handler: (job, { goTo }) => goTo({ pathname: '/home', - query: { t: 'VM', s: constructQueryString(job.vms) }, + query: { t: 'VM', s: smartModeToComplexMatcher(job.vms).toString() }, }), disabled: job => job.type !== 'backup', label: _('redirectToMatchingVms'),