diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index a70c30688..6606a1fef 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -12,6 +12,7 @@
- [Dashboard/Health] Show VMs that have too many snapshots [#5238](https://github.com/vatesfr/xen-orchestra/pull/5238)
- [Groups] Ability to delete multiple groups at once (PR [#5264](https://github.com/vatesfr/xen-orchestra/pull/5264))
- [Backup/logs] Log's tasks pagination [#4406](https://github.com/vatesfr/xen-orchestra/issues/4406) (PR [#5209](https://github.com/vatesfr/xen-orchestra/pull/5209))
+- [Backup logs] Ability to filter by VM/pool name, SRs names and remotes names [#4406](https://github.com/vatesfr/xen-orchestra/issues/4406) (PR [#5208](https://github.com/vatesfr/xen-orchestra/pull/5208))
### Bug fixes
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index 9f1a623d3..22067144c 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -83,6 +83,8 @@ const messages = {
advancedSettings: 'Advanced settings',
txChecksumming: 'TX checksumming',
unknownSize: 'Unknown size',
+ remotesNames: 'Remotes names',
+ srsNames: 'SRs names',
// ----- Modals -----
alertOk: 'OK',
diff --git a/packages/xo-web/src/common/search-bar.js b/packages/xo-web/src/common/search-bar.js
new file mode 100644
index 000000000..5ad9e2990
--- /dev/null
+++ b/packages/xo-web/src/common/search-bar.js
@@ -0,0 +1,88 @@
+import _ from 'intl'
+import classNames from 'classnames'
+import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
+import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import { Dropdown, MenuItem } from 'react-bootstrap-4/lib'
+import { Input as DebouncedInput } from 'debounce-input-decorator'
+import { isEmpty, map } from 'lodash'
+
+import Button from './button'
+import Icon from './icon'
+import Tooltip from './tooltip'
+
+export default class SearchBar extends Component {
+ static propTypes = {
+ filters: PropTypes.object,
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.string.isRequired,
+ }
+
+ _cleanFilter = () => this._setFilter('')
+
+ _setFilter = filterValue => {
+ const filter = this.refs.filter.getWrappedInstance()
+ filter.value = filterValue
+ filter.focus()
+ this.props.onChange(filterValue)
+ }
+
+ _onChange = event => {
+ this.props.onChange(event.target.value)
+ }
+
+ focus() {
+ this.refs.filter.getWrappedInstance().focus()
+ }
+
+ render() {
+ const { props } = this
+
+ return (
+
+ {isEmpty(props.filters) ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+ {map(props.filters, (filter, label) => (
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/sorted-table/index.js b/packages/xo-web/src/common/sorted-table/index.js
index 48abd8821..b273a287a 100644
--- a/packages/xo-web/src/common/sorted-table/index.js
+++ b/packages/xo-web/src/common/sorted-table/index.js
@@ -2,15 +2,11 @@ import * as CM from 'complex-matcher'
import _ from 'intl'
import classNames from 'classnames'
import defined from '@xen-orchestra/defined'
-import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
-import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import PropTypes from 'prop-types'
import React from 'react'
import Shortcuts from 'shortcuts'
-import { Input as DebouncedInput } from 'debounce-input-decorator'
import { Portal } from 'react-overlays'
import { Set } from 'immutable'
-import { Dropdown, MenuItem } from 'react-bootstrap-4/lib'
import { injectState, provideState } from 'reaclette'
import { withRouter } from 'react-router'
import {
@@ -32,7 +28,7 @@ import decorate from '../apply-decorators'
import Icon from '../icon'
import Pagination from '../pagination'
import SingleLineRow from '../single-line-row'
-import Tooltip from '../tooltip'
+import TableFilter from '../search-bar'
import { BlockLink } from '../link'
import { Container, Col } from '../grid'
import {
@@ -48,83 +44,6 @@ import styles from './index.css'
// ===================================================================
-class TableFilter extends Component {
- static propTypes = {
- filters: PropTypes.object,
- onChange: PropTypes.func.isRequired,
- value: PropTypes.string.isRequired,
- }
-
- _cleanFilter = () => this._setFilter('')
-
- _setFilter = filterValue => {
- const filter = this.refs.filter.getWrappedInstance()
- filter.value = filterValue
- filter.focus()
- this.props.onChange(filterValue)
- }
-
- _onChange = event => {
- this.props.onChange(event.target.value)
- }
-
- focus() {
- this.refs.filter.getWrappedInstance().focus()
- }
-
- render() {
- const { props } = this
-
- return (
-
- {isEmpty(props.filters) ? (
-
-
-
- ) : (
-
-
-
-
-
-
- {map(props.filters, (filter, label) => (
-
- ))}
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
- )
- }
-}
-
-// ===================================================================
-
class ColumnHead extends Component {
static propTypes = {
columnId: PropTypes.number.isRequired,
diff --git a/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-body.js b/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-body.js
index 674a792a3..befc214fc 100644
--- a/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-body.js
+++ b/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-body.js
@@ -1,4 +1,5 @@
import _, { FormattedDuration } from 'intl'
+import * as CM from 'complex-matcher'
import ActionButton from 'action-button'
import ButtonGroup from 'button-group'
import decorate from 'apply-decorators'
@@ -6,10 +7,12 @@ import defined, { get } from '@xen-orchestra/defined'
import Icon from 'icon'
import Pagination from 'pagination'
import React from 'react'
+import SearchBar from 'search-bar'
import Select from 'form/select'
import Tooltip from 'tooltip'
-import { addSubscriptions, formatSize, formatSpeed } from 'utils'
+import { addSubscriptions, connectStore, formatSize, formatSpeed } from 'utils'
import { countBy, cloneDeep, filter, keyBy, map } from 'lodash'
+import { createGetObjectsOfType } from 'selectors'
import { FormattedDate } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { runBackupNgJob, subscribeBackupNgLogs, subscribeRemotes } from 'xo'
@@ -309,6 +312,8 @@ const TaskLi = ({ className, task, ...props }) => {
)
}
+const SEARCH_BAR_FILTERS = { name: 'name:' }
+
const ITEMS_PER_PAGE = 5
export default decorate([
addSubscriptions(({ id }) => ({
@@ -321,19 +326,28 @@ export default decorate([
cb(logs[id])
}),
})),
+ connectStore({
+ pools: createGetObjectsOfType('pool'),
+ vms: createGetObjectsOfType('VM'),
+ }),
provideState({
initialState: () => ({
- filter: undefined,
+ _status: undefined,
+ filter: '',
page: 1,
}),
effects: {
onPageChange(_, page) {
this.state.page = page
},
- setFilter(_, filter) {
+ onFilterChange(_, filter) {
this.state.filter = filter
this.state.page = 1
},
+ onStatusChange(_, status) {
+ this.state._status = status
+ this.state.page = 1
+ },
restartVmJob: (_, params) => async (
_,
{ log: { scheduleId, jobId } }
@@ -347,7 +361,7 @@ export default decorate([
},
},
computed: {
- log: (_, { log }) => {
+ log: (_, { log, pools, vms }) => {
if (log === undefined) {
return {}
}
@@ -356,33 +370,41 @@ export default decorate([
return log
}
- let newLog
- log.tasks.forEach((task, key) => {
- if (task.tasks === undefined || get(() => task.data.type) !== 'VM') {
+ const newLog = cloneDeep(log)
+ newLog.tasks.forEach(task => {
+ const type = get(() => task.data.type)
+ if (type !== 'VM' && type !== 'xo' && type !== 'pool') {
return
}
- const subTaskWithIsFull = task.tasks.find(
- ({ data = {} }) => data.isFull !== undefined
- )
- if (subTaskWithIsFull !== undefined) {
- if (newLog === undefined) {
- newLog = cloneDeep(log)
- }
- newLog.tasks[key].isFull = subTaskWithIsFull.data.isFull
+ task.name =
+ type === 'VM'
+ ? get(() => vms[task.data.id].name_label)
+ : type === 'pool'
+ ? get(() => pools[task.data.id].name_label)
+ : 'xo'
+
+ if (task.tasks !== undefined) {
+ const subTaskWithIsFull = task.tasks.find(
+ ({ data = {} }) => data.isFull !== undefined
+ )
+ task.isFull = get(() => subTaskWithIsFull.data.isFull)
}
})
- return defined(newLog, log)
+ return newLog
},
- tasksFilteredByStatus: ({
- defaultFilter,
- filter: value = defaultFilter,
- log,
- }) =>
- value === 'all'
- ? log.tasks
- : filter(log.tasks, ({ status }) => status === value),
+ preFilteredTasksLogs: ({ log, filter }) => {
+ try {
+ return log.tasks.filter(CM.parse(filter).createPredicate())
+ } catch (_) {
+ return []
+ }
+ },
+ tasksFilteredByStatus: ({ preFilteredTasksLogs, status }) =>
+ status === 'all'
+ ? preFilteredTasksLogs
+ : filter(preFilteredTasksLogs, task => task.status === status),
displayedTasks: ({ tasksFilteredByStatus, page }) => {
const start = (page - 1) * ITEMS_PER_PAGE
return tasksFilteredByStatus.slice(start, start + ITEMS_PER_PAGE)
@@ -392,9 +414,9 @@ export default decorate([
{_(label)} ({countByStatus[value] || 0})
),
- countByStatus: ({ log }) => ({
- all: get(() => log.tasks.length),
- ...countBy(log.tasks, 'status'),
+ countByStatus: ({ preFilteredTasksLogs }) => ({
+ all: get(() => preFilteredTasksLogs.length),
+ ...countBy(preFilteredTasksLogs, 'status'),
}),
options: ({ countByStatus }) => [
{ label: 'allTasks', value: 'all' },
@@ -424,21 +446,22 @@ export default decorate([
value: 'success',
},
],
- defaultFilter: ({ countByStatus }) => {
- if (countByStatus.pending > 0) {
- return 'pending'
- }
+ status: ({ _status, countByStatus }) =>
+ defined(_status, () => {
+ if (countByStatus.pending > 0) {
+ return 'pending'
+ }
- if (countByStatus.failure > 0) {
- return 'failure'
- }
+ if (countByStatus.failure > 0) {
+ return 'failure'
+ }
- if (countByStatus.interrupted > 0) {
- return 'interrupted'
- }
+ if (countByStatus.interrupted > 0) {
+ return 'interrupted'
+ }
- return 'all'
- },
+ return 'all'
+ }),
nPages: ({ tasksFilteredByStatus }) =>
Math.ceil(tasksFilteredByStatus.length / ITEMS_PER_PAGE),
},
@@ -453,14 +476,20 @@ export default decorate([
) : (
+