parent
2aed2fd534
commit
45fe70f0fa
@ -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
|
||||
|
||||
|
@ -83,6 +83,8 @@ const messages = {
|
||||
advancedSettings: 'Advanced settings',
|
||||
txChecksumming: 'TX checksumming',
|
||||
unknownSize: 'Unknown size',
|
||||
remotesNames: 'Remotes names',
|
||||
srsNames: 'SRs names',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
|
88
packages/xo-web/src/common/search-bar.js
Normal file
88
packages/xo-web/src/common/search-bar.js
Normal file
@ -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 (
|
||||
<div className={classNames('input-group', props.className)}>
|
||||
{isEmpty(props.filters) ? (
|
||||
<span className='input-group-addon'>
|
||||
<Icon icon='search' />
|
||||
</span>
|
||||
) : (
|
||||
<span className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{map(props.filters, (filter, label) => (
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</span>
|
||||
)}
|
||||
<DebouncedInput
|
||||
className='form-control'
|
||||
onChange={this._onChange}
|
||||
ref='filter'
|
||||
value={props.value}
|
||||
/>
|
||||
<Tooltip content={_('filterSyntaxLinkTooltip')}>
|
||||
<a
|
||||
className='input-group-addon'
|
||||
href='https://xen-orchestra.com/docs/manage_infrastructure.html#live-filter-search'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<span className='input-group-btn'>
|
||||
<Button onClick={this._cleanFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<div className='input-group'>
|
||||
{isEmpty(props.filters) ? (
|
||||
<span className='input-group-addon'>
|
||||
<Icon icon='search' />
|
||||
</span>
|
||||
) : (
|
||||
<span className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{map(props.filters, (filter, label) => (
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</span>
|
||||
)}
|
||||
<DebouncedInput
|
||||
className='form-control'
|
||||
onChange={this._onChange}
|
||||
ref='filter'
|
||||
value={props.value}
|
||||
/>
|
||||
<Tooltip content={_('filterSyntaxLinkTooltip')}>
|
||||
<a
|
||||
className='input-group-addon'
|
||||
href='https://xen-orchestra.com/docs/manage_infrastructure.html#live-filter-search'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<span className='input-group-btn'>
|
||||
<Button onClick={this._cleanFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class ColumnHead extends Component {
|
||||
static propTypes = {
|
||||
columnId: PropTypes.number.isRequired,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
if (subTaskWithIsFull !== undefined) {
|
||||
if (newLog === undefined) {
|
||||
newLog = cloneDeep(log)
|
||||
}
|
||||
newLog.tasks[key].isFull = subTaskWithIsFull.data.isFull
|
||||
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})
|
||||
</span>
|
||||
),
|
||||
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,7 +446,8 @@ export default decorate([
|
||||
value: 'success',
|
||||
},
|
||||
],
|
||||
defaultFilter: ({ countByStatus }) => {
|
||||
status: ({ _status, countByStatus }) =>
|
||||
defined(_status, () => {
|
||||
if (countByStatus.pending > 0) {
|
||||
return 'pending'
|
||||
}
|
||||
@ -438,7 +461,7 @@ export default decorate([
|
||||
}
|
||||
|
||||
return 'all'
|
||||
},
|
||||
}),
|
||||
nPages: ({ tasksFilteredByStatus }) =>
|
||||
Math.ceil(tasksFilteredByStatus.length / ITEMS_PER_PAGE),
|
||||
},
|
||||
@ -453,14 +476,20 @@ export default decorate([
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<SearchBar
|
||||
className='mb-1'
|
||||
filters={SEARCH_BAR_FILTERS}
|
||||
onChange={effects.onFilterChange}
|
||||
value={state.filter}
|
||||
/>
|
||||
<Select
|
||||
labelKey='label'
|
||||
onChange={effects.setFilter}
|
||||
onChange={effects.onStatusChange}
|
||||
optionRenderer={state.optionRenderer}
|
||||
options={state.options}
|
||||
required
|
||||
simpleValue
|
||||
value={state.filter || state.defaultFilter}
|
||||
value={state.status}
|
||||
valueKey='value'
|
||||
/>
|
||||
<Warnings warnings={warnings} />
|
||||
|
Loading…
Reference in New Issue
Block a user