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) => ( + this._setFilter(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) => ( - this._setFilter(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([ ) : (
+