parent
cc26e378e5
commit
48727740c4
@ -11,6 +11,7 @@
|
||||
- [Servers] Auto-connect to ejected host [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3738](https://github.com/vatesfr/xen-orchestra/pull/3738))
|
||||
- [Backup NG] Add "XOSAN" in excluded tags by default [#2128](https://github.com/vatesfr/xen-orchestra/issues/3563) (PR [#3559](https://github.com/vatesfr/xen-orchestra/pull/3563))
|
||||
- [VM] add tooltip for VM status icon [#3749](https://github.com/vatesfr/xen-orchestra/issues/3749) (PR [#3765](https://github.com/vatesfr/xen-orchestra/pull/3765))
|
||||
- [New XOSAN] Improve view and possibility to sort SRs by name/size/free space [#2416](https://github.com/vatesfr/xen-orchestra/issues/2416) (PR [#3691](https://github.com/vatesfr/xen-orchestra/pull/3691))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
@ -1947,7 +1947,7 @@ const messages = {
|
||||
xosanRestartAgents: 'Restart toolstacks',
|
||||
xosanMasterOffline: 'Pool master is not running',
|
||||
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
|
||||
xosanSelect2Srs: 'Select at least 2 SRs',
|
||||
xosanSrOnSameHostMessage: 'Select no more than 1 SR per host',
|
||||
xosanLayout: 'Layout',
|
||||
xosanRedundancy: 'Redundancy',
|
||||
xosanCapacity: 'Capacity',
|
||||
|
@ -71,7 +71,8 @@ export const Host = decorate([
|
||||
pool: createGetObject(
|
||||
createSelector(
|
||||
getHost,
|
||||
host => get(() => host.$pool)
|
||||
(_, props) => props.pool,
|
||||
(host, showPool) => showPool && get(() => host.$pool)
|
||||
)
|
||||
),
|
||||
}
|
||||
@ -94,11 +95,13 @@ Host.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
link: PropTypes.bool,
|
||||
newTab: PropTypes.bool,
|
||||
pool: PropTypes.bool,
|
||||
}
|
||||
|
||||
Host.defaultProps = {
|
||||
link: false,
|
||||
newTab: false,
|
||||
pool: true,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@ -183,7 +186,8 @@ export const Sr = decorate([
|
||||
const getContainer = createGetObject(
|
||||
createSelector(
|
||||
getSr,
|
||||
sr => get(() => sr.$container)
|
||||
(_, props) => props.container,
|
||||
(sr, showContainer) => showContainer && get(() => sr.$container)
|
||||
)
|
||||
)
|
||||
return (state, props) => ({
|
||||
@ -217,6 +221,7 @@ export const Sr = decorate([
|
||||
])
|
||||
|
||||
Sr.propTypes = {
|
||||
container: PropTypes.bool,
|
||||
id: PropTypes.string.isRequired,
|
||||
link: PropTypes.bool,
|
||||
newTab: PropTypes.bool,
|
||||
@ -225,6 +230,7 @@ Sr.propTypes = {
|
||||
}
|
||||
|
||||
Sr.defaultProps = {
|
||||
container: true,
|
||||
link: false,
|
||||
newTab: false,
|
||||
self: false,
|
||||
|
@ -71,11 +71,11 @@ class TableFilter extends Component {
|
||||
this.props.onChange(event.target.value)
|
||||
}
|
||||
|
||||
focus () {
|
||||
focus() {
|
||||
this.refs.filter.getWrappedInstance().focus()
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
@ -140,7 +140,7 @@ class ColumnHead extends Component {
|
||||
props.sort(props.columnId)
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { name, sortIcon, textAlign } = this.props
|
||||
|
||||
if (!this.props.sort) {
|
||||
@ -174,7 +174,7 @@ class Checkbox extends Component {
|
||||
indeterminate: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
props: { indeterminate },
|
||||
ref,
|
||||
@ -189,7 +189,7 @@ class Checkbox extends Component {
|
||||
this.componentDidUpdate()
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { indeterminate, ...props } = this.props
|
||||
props.ref = this._ref
|
||||
props.type = 'checkbox'
|
||||
@ -295,6 +295,7 @@ export default class SortedTable extends Component {
|
||||
groupedActions: actionsShape,
|
||||
individualActions: actionsShape,
|
||||
itemsPerPage: PropTypes.number,
|
||||
onSelect: PropTypes.func,
|
||||
paginationContainer: PropTypes.func,
|
||||
rowAction: PropTypes.func,
|
||||
rowLink: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
@ -316,7 +317,7 @@ export default class SortedTable extends Component {
|
||||
router: routerShape,
|
||||
}
|
||||
|
||||
constructor (props, context) {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
|
||||
this._getUserData =
|
||||
@ -505,7 +506,7 @@ export default class SortedTable extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this._checkUpdatePage()
|
||||
|
||||
// Force one Portal refresh.
|
||||
@ -532,7 +533,7 @@ export default class SortedTable extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
componentDidUpdate() {
|
||||
const { selectedItemsIds } = this.state
|
||||
|
||||
// Unselect items that are no longer visible
|
||||
@ -567,7 +568,7 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
|
||||
// update state in the state and update the URL param
|
||||
_setVisibleState (state) {
|
||||
_setVisibleState(state) {
|
||||
this.setState(state, this.props.stateUrlParam && this._saveUrlState)
|
||||
}
|
||||
|
||||
@ -579,7 +580,7 @@ export default class SortedTable extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
_checkUpdatePage () {
|
||||
_checkUpdatePage() {
|
||||
const { page } = this.state
|
||||
if (page === 1) {
|
||||
return
|
||||
@ -597,15 +598,21 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_setPage (page) {
|
||||
_setPage(page) {
|
||||
this._setVisibleState({ page })
|
||||
}
|
||||
_setPage = this._setPage.bind(this)
|
||||
|
||||
_selectAllVisibleItems = event => {
|
||||
const { checked } = event.target
|
||||
const { onSelect } = this.props
|
||||
if (onSelect !== undefined) {
|
||||
onSelect(checked ? map(this._getVisibleItems(), 'id') : [])
|
||||
}
|
||||
|
||||
this.setState({
|
||||
all: false,
|
||||
selectedItemsIds: event.target.checked
|
||||
selectedItemsIds: checked
|
||||
? this.state.selectedItemsIds.union(map(this._getVisibleItems(), 'id'))
|
||||
: this.state.selectedItemsIds.clear(),
|
||||
})
|
||||
@ -626,34 +633,37 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_selectAll = () => this.setState({ all: true })
|
||||
_selectAll = () => {
|
||||
const { onSelect } = this.props
|
||||
if (onSelect !== undefined) {
|
||||
onSelect(map(this._getItems(), 'id'))
|
||||
}
|
||||
this.setState({ all: true })
|
||||
}
|
||||
|
||||
_selectItem (current, selected, range = false) {
|
||||
_selectItem(current, selected, range = false) {
|
||||
const { onSelect } = this.props
|
||||
const { all, selectedItemsIds } = this.state
|
||||
const visibleItems = this._getVisibleItems()
|
||||
const item = visibleItems[current]
|
||||
let _selectedItemsIds
|
||||
|
||||
if (all) {
|
||||
return this.setState({
|
||||
all: false,
|
||||
selectedItemsIds: new Set().withMutations(selectedItemsIds => {
|
||||
forEach(visibleItems, item => {
|
||||
selectedItemsIds.add(item.id)
|
||||
})
|
||||
selectedItemsIds.delete(item.id)
|
||||
}),
|
||||
_selectedItemsIds = new Set().withMutations(selectedItemsIds => {
|
||||
forEach(visibleItems, item => {
|
||||
selectedItemsIds.add(item.id)
|
||||
})
|
||||
selectedItemsIds.delete(item.id)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const method = (selected === undefined
|
||||
? !selectedItemsIds.has(item.id)
|
||||
: selected)
|
||||
? 'add'
|
||||
: 'delete'
|
||||
|
||||
const method = (selected === undefined
|
||||
? !selectedItemsIds.has(item.id)
|
||||
: selected)
|
||||
? 'add'
|
||||
: 'delete'
|
||||
|
||||
let previous
|
||||
this.setState({
|
||||
selectedItemsIds:
|
||||
let previous
|
||||
_selectedItemsIds =
|
||||
range && (previous = this._previous) !== undefined
|
||||
? selectedItemsIds.withMutations(selectedItemsIds => {
|
||||
let i = previous
|
||||
@ -666,10 +676,18 @@ export default class SortedTable extends Component {
|
||||
selectedItemsIds[method](visibleItems[i].id)
|
||||
}
|
||||
})
|
||||
: selectedItemsIds[method](item.id),
|
||||
})
|
||||
: selectedItemsIds[method](item.id)
|
||||
this._previous = current
|
||||
}
|
||||
|
||||
this._previous = current
|
||||
if (onSelect !== undefined) {
|
||||
onSelect(_selectedItemsIds.toArray())
|
||||
}
|
||||
|
||||
this.setState({
|
||||
all: false,
|
||||
selectedItemsIds: _selectedItemsIds,
|
||||
})
|
||||
}
|
||||
|
||||
_onSelectItemCheckbox = event => {
|
||||
@ -717,7 +735,7 @@ export default class SortedTable extends Component {
|
||||
|
||||
_renderItem = (item, i) => {
|
||||
const { props, state } = this
|
||||
const { actions, individualActions, rowAction, rowLink } = props
|
||||
const { actions, individualActions, onSelect, rowAction, rowLink } = props
|
||||
const userData = this._getUserData()
|
||||
|
||||
const hasGroupedActions = this._hasGroupedActions()
|
||||
@ -741,7 +759,7 @@ export default class SortedTable extends Component {
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
const selectionColumn = hasGroupedActions && (
|
||||
const selectionColumn = (hasGroupedActions || onSelect !== undefined) && (
|
||||
<td className='text-xs-center' onClick={this._toggleNestedCheckbox}>
|
||||
<input
|
||||
checked={state.all || state.selectedItemsIds.has(id)}
|
||||
@ -790,13 +808,14 @@ export default class SortedTable extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { props, state } = this
|
||||
const {
|
||||
actions,
|
||||
filterContainer,
|
||||
individualActions,
|
||||
itemsPerPage,
|
||||
onSelect,
|
||||
paginationContainer,
|
||||
shortcutsTarget,
|
||||
} = props
|
||||
@ -903,7 +922,7 @@ export default class SortedTable extends Component {
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{hasGroupedActions && (
|
||||
{(hasGroupedActions || onSelect !== undefined) && (
|
||||
<th
|
||||
className='text-xs-center'
|
||||
onClick={this._toggleNestedCheckbox}
|
||||
|
@ -6,21 +6,14 @@ import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { Host, Sr } from 'render-xo-item'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { Toggle, SizeInput } from 'form'
|
||||
import { SelectPif, SelectPool } from 'select-objects'
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
groupBy,
|
||||
isEmpty,
|
||||
keys,
|
||||
map,
|
||||
pickBy,
|
||||
} from 'lodash'
|
||||
import { filter, forEach, groupBy, isEmpty, map, pickBy, some } from 'lodash'
|
||||
import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
@ -62,6 +55,45 @@ const _findLatestTemplate = templates => {
|
||||
const DEFAULT_BRICKSIZE = 100 * 1024 * 1024 * 1024 // 100 GiB
|
||||
const DEFAULT_MEMORY = 2 * 1024 * 1024 * 1024 // 2 GiB
|
||||
|
||||
const XOSAN_SR_COLUMNS = [
|
||||
{
|
||||
itemRenderer: sr => (
|
||||
<Sr id={sr.id} container={false} spaceLeft={false} link />
|
||||
),
|
||||
name: _('xosanName'),
|
||||
sortCriteria: 'name_label',
|
||||
},
|
||||
{
|
||||
itemRenderer: sr => <Host id={sr.$container} pool={false} link />,
|
||||
name: _('xosanHost'),
|
||||
sortCriteria: (sr, { hosts }) => hosts[sr.$container].name_label,
|
||||
},
|
||||
{
|
||||
itemRenderer: sr => <span>{formatSize(sr.size)}</span>,
|
||||
name: _('xosanSize'),
|
||||
sortCriteria: 'size',
|
||||
},
|
||||
{
|
||||
itemRenderer: sr =>
|
||||
sr.size > 0 && (
|
||||
<Tooltip
|
||||
content={_('spaceLeftTooltip', {
|
||||
used: String(Math.round((sr.physical_usage / sr.size) * 100)),
|
||||
free: formatSize(sr.size - sr.physical_usage),
|
||||
})}
|
||||
>
|
||||
<progress
|
||||
className='progress'
|
||||
max='100'
|
||||
value={(sr.physical_usage / sr.size) * 100}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
name: _('xosanUsedSpace'),
|
||||
sortCriteria: sr => sr.size - sr.physical_usage,
|
||||
},
|
||||
]
|
||||
|
||||
@addSubscriptions({
|
||||
catalog: subscribeResourceCatalog,
|
||||
})
|
||||
@ -72,7 +104,7 @@ const DEFAULT_MEMORY = 2 * 1024 * 1024 * 1024 // 2 GiB
|
||||
})
|
||||
export default class NewXosan extends Component {
|
||||
state = {
|
||||
selectedSrs: {},
|
||||
selectedSrs: [],
|
||||
brickSize: DEFAULT_BRICKSIZE,
|
||||
ipRange: '172.31.100.0',
|
||||
memorySize: DEFAULT_MEMORY,
|
||||
@ -116,13 +148,13 @@ export default class NewXosan extends Component {
|
||||
needsUpdate: false,
|
||||
pif: undefined,
|
||||
pool,
|
||||
selectedSrs: {},
|
||||
selectedSrs: [],
|
||||
})
|
||||
|
||||
return this._checkPacks(pool)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
componentDidUpdate() {
|
||||
this._refreshSuggestions()
|
||||
}
|
||||
|
||||
@ -131,13 +163,16 @@ export default class NewXosan extends Component {
|
||||
() => this.state.selectedSrs,
|
||||
() => this.state.brickSize,
|
||||
() => this.state.customBrickSize,
|
||||
async (selectedSrs, brickSize, customBrickSize) => {
|
||||
() => this.state.srsOnSameHost,
|
||||
async (selectedSrs, brickSize, customBrickSize, srsOnSameHost) => {
|
||||
this.setState({
|
||||
suggestion: 0,
|
||||
suggestions: await computeXosanPossibleOptions(
|
||||
keys(pickBy(selectedSrs)),
|
||||
customBrickSize ? brickSize : undefined
|
||||
),
|
||||
suggestions: !srsOnSameHost
|
||||
? await computeXosanPossibleOptions(
|
||||
selectedSrs,
|
||||
customBrickSize ? brickSize : undefined
|
||||
)
|
||||
: [],
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -156,7 +191,7 @@ export default class NewXosan extends Component {
|
||||
_getHosts = createSelector(
|
||||
() => this.props.hosts,
|
||||
this._getIsInPool,
|
||||
(hosts, isInPool) => filter(hosts, isInPool)
|
||||
(hosts, isInPool) => pickBy(hosts, isInPool)
|
||||
)
|
||||
|
||||
// LVM SRs that are connected
|
||||
@ -164,14 +199,17 @@ export default class NewXosan extends Component {
|
||||
createSelector(
|
||||
createFilter(
|
||||
() => this.props.srs,
|
||||
createSelector(this._getHosts, hosts => sr => {
|
||||
let host
|
||||
return (
|
||||
sr.SR_type === 'lvm' &&
|
||||
(host = find(hosts, { id: sr.$container })) !== undefined &&
|
||||
host.power_state === 'Running'
|
||||
)
|
||||
})
|
||||
createSelector(
|
||||
this._getHosts,
|
||||
hosts => sr => {
|
||||
let host
|
||||
return (
|
||||
sr.SR_type === 'lvm' &&
|
||||
(host = hosts[sr.$container]) !== undefined &&
|
||||
host.power_state === 'Running'
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
this._getPbdsBySr,
|
||||
(srs, pbdsBySr) =>
|
||||
@ -195,10 +233,17 @@ export default class NewXosan extends Component {
|
||||
this.setState({ brickSize })
|
||||
}
|
||||
|
||||
_selectSr = async (event, sr) => {
|
||||
const selectedSrs = { ...this.state.selectedSrs }
|
||||
selectedSrs[sr.id] = event.target.checked
|
||||
this.setState({ selectedSrs })
|
||||
_selectSrs = selectedSrs => {
|
||||
const { srs } = this.props
|
||||
const found = {}
|
||||
let container
|
||||
this.setState({
|
||||
selectedSrs,
|
||||
srsOnSameHost: some(selectedSrs, srId => {
|
||||
container = get(() => srs[srId].$container)
|
||||
return found[container] || ((found[container] = true), false)
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
_getPifPredicate = createSelector(
|
||||
@ -208,7 +253,7 @@ export default class NewXosan extends Component {
|
||||
|
||||
_getNSelectedSrs = createSelector(
|
||||
() => this.state.selectedSrs,
|
||||
srs => filter(srs).length
|
||||
srs => srs.length
|
||||
)
|
||||
|
||||
_getLatestTemplate = createSelector(
|
||||
@ -218,24 +263,14 @@ export default class NewXosan extends Component {
|
||||
_findLatestTemplate
|
||||
)
|
||||
|
||||
_getDisableSrCheckbox = createSelector(
|
||||
() => this.state.selectedSrs,
|
||||
this._getLvmSrs,
|
||||
(selectedSrs, lvmsrs) => sr =>
|
||||
!every(
|
||||
keys(pickBy(selectedSrs)),
|
||||
selectedSrId =>
|
||||
selectedSrId === sr.id ||
|
||||
find(lvmsrs, { id: selectedSrId }).$container !== sr.$container
|
||||
)
|
||||
)
|
||||
|
||||
_getDisableCreation = createSelector(
|
||||
() => this.state.srsOnSameHost,
|
||||
() => this.state.suggestion,
|
||||
() => this.state.suggestions,
|
||||
() => this.state.pif,
|
||||
this._getNSelectedSrs,
|
||||
(suggestion, suggestions, pif, nSelectedSrs) =>
|
||||
(srsOnSameHost, suggestion, suggestions, pif, nSelectedSrs) =>
|
||||
srsOnSameHost ||
|
||||
!suggestions ||
|
||||
!suggestions[suggestion] ||
|
||||
!pif ||
|
||||
@ -254,7 +289,7 @@ export default class NewXosan extends Component {
|
||||
template: this._getLatestTemplate(),
|
||||
pif: this.state.pif,
|
||||
vlan: this.state.vlan || 0,
|
||||
srs: keys(pickBy(this.state.selectedSrs)),
|
||||
srs: this.state.selectedSrs,
|
||||
glusterType: params.layout,
|
||||
redundancy: params.redundancy,
|
||||
brickSize: this.state.customBrickSize ? this.state.brickSize : undefined,
|
||||
@ -265,7 +300,7 @@ export default class NewXosan extends Component {
|
||||
this.props.onSrCreationStarted()
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (process.env.XOA_PLAN === 5) {
|
||||
return (
|
||||
<em>
|
||||
@ -288,7 +323,7 @@ export default class NewXosan extends Component {
|
||||
needsUpdate,
|
||||
pif,
|
||||
pool,
|
||||
selectedSrs,
|
||||
srsOnSameHost,
|
||||
suggestion,
|
||||
suggestions,
|
||||
useVlan,
|
||||
@ -307,12 +342,8 @@ export default class NewXosan extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
const lvmsrs = this._getLvmSrs()
|
||||
const hosts = this._getHosts()
|
||||
|
||||
const disableSrCheckbox = this._getDisableSrCheckbox()
|
||||
const hostsNeedRestart =
|
||||
pool !== undefined &&
|
||||
pool != null &&
|
||||
hostsNeedRestartByPool !== undefined &&
|
||||
hostsNeedRestartByPool[pool.id]
|
||||
const architecture = suggestions != null && suggestions[suggestion]
|
||||
@ -375,69 +406,21 @@ export default class NewXosan extends Component {
|
||||
[
|
||||
<Row>
|
||||
<Col>
|
||||
<em>{_('xosanSelect2Srs')}</em>
|
||||
<table className='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{_('xosanName')}</th>
|
||||
<th>{_('xosanHost')}</th>
|
||||
<th>{_('xosanSize')}</th>
|
||||
<th>{_('xosanUsedSpace')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(lvmsrs, sr => {
|
||||
const host = find(hosts, ['id', sr.$container])
|
||||
|
||||
return (
|
||||
<tr key={sr.id}>
|
||||
<td>
|
||||
<input
|
||||
checked={selectedSrs[sr.id] || false}
|
||||
disabled={disableSrCheckbox(sr)}
|
||||
onChange={event => this._selectSr(event, sr)}
|
||||
type='checkbox'
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/srs/${sr.id}/general`}>
|
||||
{sr.name_label}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/hosts/${host.id}/general`}>
|
||||
{host.name_label}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{formatSize(sr.size)}</td>
|
||||
<td>
|
||||
{sr.size > 0 && (
|
||||
<Tooltip
|
||||
content={_('spaceLeftTooltip', {
|
||||
used: String(
|
||||
Math.round(
|
||||
(sr.physical_usage / sr.size) * 100
|
||||
)
|
||||
),
|
||||
free: formatSize(
|
||||
sr.size - sr.physical_usage
|
||||
),
|
||||
})}
|
||||
>
|
||||
<progress
|
||||
className='progress'
|
||||
max='100'
|
||||
value={(sr.physical_usage / sr.size) * 100}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<SortedTable
|
||||
collection={this._getLvmSrs()}
|
||||
columns={XOSAN_SR_COLUMNS}
|
||||
data-hosts={this._getHosts()}
|
||||
onSelect={this._selectSrs}
|
||||
/>
|
||||
</Col>
|
||||
</Row>,
|
||||
<Row>
|
||||
<Col>
|
||||
{srsOnSameHost && (
|
||||
<span className='text-danger'>
|
||||
<Icon icon='alarm' /> {_('xosanSrOnSameHostMessage')}
|
||||
</span>
|
||||
)}
|
||||
</Col>
|
||||
</Row>,
|
||||
<Row>
|
||||
|
Loading…
Reference in New Issue
Block a user