feat(xo-web/render-xo-item): simpler item components (#3547)

See #2605
This commit is contained in:
Pierre Donias 2018-12-03 11:11:55 +01:00 committed by GitHub
parent 55d7a1def0
commit 56a2f8858b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 322 additions and 199 deletions

View File

@ -2,7 +2,7 @@ import _ from 'intl'
import PropTypes from 'prop-types'
import React from 'react'
import { get } from '@xen-orchestra/defined'
import { startsWith } from 'lodash'
import { find, startsWith } from 'lodash'
import decorate from './apply-decorators'
import Icon from './icon'
@ -14,186 +14,314 @@ import { isSrWritable, subscribeRemotes } from './xo'
// ===================================================================
const OBJECT_TYPE_TO_ICON = {
'VM-template': 'vm',
host: 'host',
network: 'network',
}
const UNKNOWN_ITEM = <span className='text-muted'>{_('errorUnknownItem')}</span>
const COMMON_PROP_TYPES = {
link: PropTypes.bool,
}
const XoItem = ({ children, item, link, to, newTab }) =>
item !== undefined ? (
link ? (
<Link to={to} target={newTab && '_blank'}>
{children()}
</Link>
) : (
children()
)
const LinkWrapper = ({ children, link, to, newTab }) =>
link ? (
<Link to={to} target={newTab && '_blank'}>
{children}
</Link>
) : (
<span className='text-muted'>{_('errorUnknownItem')}</span>
<span>{children}</span>
)
XoItem.propTypes = {
...COMMON_PROP_TYPES,
item: PropTypes.object,
LinkWrapper.propTypes = {
link: PropTypes.bool,
newTab: PropTypes.bool,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
}
// ===================================================================
const XO_ITEM_PROP_TYPES = {
...COMMON_PROP_TYPES,
export const Pool = decorate([
connectStore(() => ({
pool: createGetObject(),
})),
({ pool, link, newTab }) => {
if (pool === undefined) {
return UNKNOWN_ITEM
}
return (
<LinkWrapper link={link} newTab={newTab} to={`/pools/${pool.id}`}>
<Icon icon='pool' /> {pool.name_label}
</LinkWrapper>
)
},
])
Pool.propTypes = {
id: PropTypes.string.isRequired,
link: PropTypes.bool,
newTab: PropTypes.bool,
}
export const VmItem = decorate([
Pool.defaultProps = {
link: false,
newTab: false,
}
// ===================================================================
export const Host = decorate([
connectStore(() => {
const getHost = createGetObject()
return {
host: getHost,
pool: createGetObject(
createSelector(
getHost,
host => get(() => host.$pool)
)
),
}
}),
({ host, pool, link, newTab }) => {
if (host === undefined) {
return UNKNOWN_ITEM
}
return (
<LinkWrapper link={link} newTab={newTab} to={`/hosts/${host.id}`}>
<Icon icon='host' /> {host.name_label}
{pool !== undefined && ` (${pool.name_label})`}
</LinkWrapper>
)
},
])
Host.propTypes = {
id: PropTypes.string.isRequired,
link: PropTypes.bool,
newTab: PropTypes.bool,
}
Host.defaultProps = {
link: false,
newTab: false,
}
// ===================================================================
export const Vm = decorate([
connectStore(() => {
const getVm = createGetObject()
return {
vm: getVm,
container: createGetObject(
createSelector(getVm, vm => get(() => vm.$container))
createSelector(
getVm,
vm => get(() => vm.$container)
)
),
}
}),
({ vm, container, ...props }) => (
<XoItem item={vm} to={`/vms/${get(() => vm.id)}`} {...props}>
{() => (
<span>
<Icon icon={`vm-${vm.power_state.toLowerCase()}`} />{' '}
{vm.name_label || vm.id}
{container !== undefined &&
` (${container.name_label || container.id})`}
</span>
)}
</XoItem>
),
])
VmItem.propTypes = XO_ITEM_PROP_TYPES
export const SrItem = decorate([
connectStore(() => {
const getSr = createGetObject()
return {
sr: getSr,
container: createGetObject(
createSelector(getSr, sr => get(() => sr.$container))
),
({ vm, container, link, newTab }) => {
if (vm === undefined) {
return UNKNOWN_ITEM
}
}),
({ sr, container, ...props }) => (
<XoItem item={sr} to={`/srs/${get(() => sr.id)}`} {...props}>
{() => (
<span>
<Icon icon='sr' /> {sr.name_label || sr.id}
{container !== undefined && (
<span className='text-muted'> - {container.name_label}</span>
)}
{isSrWritable(sr) && (
<span>{` (${formatSize(sr.size - sr.physical_usage)} free)`}</span>
)}
</span>
)}
</XoItem>
),
return (
<LinkWrapper link={link} newTab={newTab} to={`/vms/${vm.id}`}>
<Icon icon={`vm-${vm.power_state.toLowerCase()}`} /> {vm.name_label}
{container !== undefined && ` (${container.name_label})`}
</LinkWrapper>
)
},
])
SrItem.propTypes = XO_ITEM_PROP_TYPES
export const RemoteItem = decorate([
addSubscriptions(({ id }) => ({
remote: cb =>
subscribeRemotes(remotes => {
cb(get(() => remotes.find(remote => remote.id === id)))
}),
})),
({ remote, ...props }) => (
<XoItem item={remote} to='/settings/remotes' {...props}>
{() => (
<span>
<Icon icon='remote' /> {remote.name}
</span>
)}
</XoItem>
),
])
RemoteItem.propTypes = XO_ITEM_PROP_TYPES
export const PoolItem = decorate([
connectStore(() => ({
pool: createGetObject(),
})),
({ pool, ...props }) => (
<XoItem item={pool} to={`/pools/${get(() => pool.id)}`} {...props}>
{() => (
<span>
<Icon icon='pool' /> {pool.name_label || pool.id}
</span>
)}
</XoItem>
),
])
PoolItem.propTypes = XO_ITEM_PROP_TYPES
// ===================================================================
export const SrResourceSetItem = decorate([
connectStore(() => {
const getSr = createGetObject()
return (state, props) => ({
// true to bypass permissions as a self user
sr: getSr(state, props, true),
})
}),
({ sr, ...props }) => (
<XoItem item={sr} to={sr !== undefined && `/srs/${sr.id}`} {...props}>
{() => (
<span>
<Icon icon='sr' /> {sr.name_label || sr.id}
{isSrWritable(sr) && (
<span>{` (${formatSize(sr.size - sr.physical_usage)} free)`}</span>
)}
</span>
)}
</XoItem>
),
])
SrResourceSetItem.propTypes = XO_ITEM_PROP_TYPES
// ===================================================================
// Host, Network, VM-template.
const PoolObjectItem = connectStore(() => {
const getPool = createGetObject((_, props) => props.object.$pool)
return (state, props) => ({
pool: getPool(state, props),
})
})(({ object, pool }) => {
const icon = OBJECT_TYPE_TO_ICON[object.type]
const { id } = object
return (
<span>
<Icon icon={icon} /> {`${object.name_label || id} `}
{pool && `(${pool.name_label || pool.id})`}
</span>
)
})
PoolObjectItem.propTypes = {
object: PropTypes.object.isRequired,
Vm.propTypes = {
id: PropTypes.string.isRequired,
link: PropTypes.bool,
newTab: PropTypes.bool,
}
const VgpuItem = connectStore(() => ({
Vm.defaultProps = {
link: false,
newTab: false,
}
// ===================================================================
export const VmTemplate = decorate([
connectStore(() => {
const getObject = createGetObject()
return (state, props) => ({
// FIXME: props.self ugly workaround to get object as a self user
template: getObject(state, props, props.self),
})
}),
({ template }) => {
if (template === undefined) {
return UNKNOWN_ITEM
}
return (
<span>
<Icon icon='vm' /> {template.name_label}
</span>
)
},
])
VmTemplate.propTypes = {
id: PropTypes.string.isRequired,
self: PropTypes.bool,
}
VmTemplate.defaultProps = {
link: false,
newTab: false,
self: false,
}
// ===================================================================
export const Sr = decorate([
connectStore(() => {
const getSr = createGetObject()
const getContainer = createGetObject(
createSelector(
getSr,
sr => get(() => sr.$container)
)
)
return (state, props) => ({
// FIXME: props.self ugly workaround to get object as a self user
sr: getSr(state, props, props.self),
container: getContainer(state, props),
})
}),
({ sr, container, link, newTab }) => {
if (sr === undefined) {
return UNKNOWN_ITEM
}
return (
<LinkWrapper link={link} newTab={newTab} to={`/srs/${sr.id}`}>
<Icon icon='sr' /> {sr.name_label}
{container !== undefined && (
<span className='text-muted'> - {container.name_label}</span>
)}
{isSrWritable(sr) && (
<span>{` (${formatSize(sr.size - sr.physical_usage)} free)`}</span>
)}
</LinkWrapper>
)
},
])
Sr.propTypes = {
id: PropTypes.string.isRequired,
link: PropTypes.bool,
newTab: PropTypes.bool,
self: PropTypes.bool,
}
Sr.defaultProps = {
link: false,
newTab: false,
self: false,
}
// ===================================================================
export const Vdi = decorate([
connectStore(() => {
const getObject = createGetObject()
// FIXME: props.self ugly workaround to get object as a self user
return (state, props) => ({
vdi: getObject(state, props, props.self),
})
}),
({ vdi }) => {
if (vdi === undefined) {
return UNKNOWN_ITEM
}
return (
<span>
<Icon icon='disk' /> {vdi.name_label}
{vdi.name_description && <span> ({vdi.name_description})</span>}
</span>
)
},
])
Vdi.propTypes = {
id: PropTypes.string.isRequired,
self: PropTypes.bool,
}
Vdi.defaultProps = {
self: false,
}
// ===================================================================
export const Network = decorate([
connectStore(() => {
const getObject = createGetObject()
// FIXME: props.self ugly workaround to get object as a self user
return (state, props) => ({
network: getObject(state, props, props.self),
})
}),
({ network }) => {
if (network === undefined) {
return UNKNOWN_ITEM
}
return (
<span>
<Icon icon='network' /> {network.name_label}
</span>
)
},
])
Network.propTypes = {
id: PropTypes.string.isRequired,
self: PropTypes.bool,
}
Network.defaultProps = {
self: false,
}
// ===================================================================
export const Remote = decorate([
addSubscriptions(({ id }) => ({
remote: cb => subscribeRemotes(remotes => cb(find(remotes, { id }))),
})),
({ remote, link, newTab }) => {
if (remote === undefined) {
return UNKNOWN_ITEM // TODO: handle remotes not fetched yet
}
return (
<LinkWrapper link={link} newTab={newTab} to='/settings/remotes'>
<Icon icon='remote' /> {remote.name}
</LinkWrapper>
)
},
])
Remote.propTypes = {
id: PropTypes.string.isRequired,
link: PropTypes.bool,
newTab: PropTypes.bool,
}
Remote.defaultProps = {
link: false,
newTab: false,
}
// ===================================================================
export const Vgpu = connectStore(() => ({
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
}))(({ vgpu, vgpuType }) => (
<span>
@ -201,6 +329,10 @@ const VgpuItem = connectStore(() => ({
</span>
))
Vgpu.propTypes = {
vgpu: PropTypes.object.isRequired,
}
// ===================================================================
const xoItemToRender = {
@ -215,7 +347,7 @@ const xoItemToRender = {
<Icon icon='group' /> {group.name}
</span>
),
remote: ({ value: { id } }) => <RemoteItem id={id} />,
remote: ({ value: { id } }) => <Remote id={id} />,
role: role => <span>{role.name}</span>,
user: user => (
<span>
@ -248,30 +380,28 @@ const xoItemToRender = {
},
// XO objects.
pool: ({ id }) => <PoolItem id={id} />,
pool: ({ id }) => <Pool id={id} />,
VDI: vdi => (
<span>
<Icon icon='disk' /> {vdi.name_label}{' '}
{vdi.name_description && <span> ({vdi.name_description})</span>}
</span>
),
VDI: ({ id }) => <Vdi id={id} />,
'VDI-resourceSet': ({ id }) => <Vdi id={id} self />,
// Pool objects.
'VM-template': vmTemplate => <PoolObjectItem object={vmTemplate} />,
host: host => <PoolObjectItem object={host} />,
network: network => <PoolObjectItem object={network} />,
'VM-template': ({ id }) => <VmTemplate id={id} />,
'VM-template-resourceSet': ({ id }) => <VmTemplate id={id} self />,
host: ({ id }) => <Host id={id} />,
network: ({ id }) => <Network id={id} />,
'network-resourceSet': ({ id }) => <Network id={id} self />,
// SR.
SR: ({ id }) => <SrItem id={id} />,
'SR-resourceSet': ({ id }) => <SrResourceSetItem id={id} />,
SR: ({ id }) => <Sr id={id} />,
'SR-resourceSet': ({ id }) => <Sr id={id} self />,
// VM.
VM: ({ id }) => <VmItem id={id} />,
'VM-snapshot': ({ id }) => <VmItem id={id} />,
VM: ({ id }) => <Vm id={id} />,
'VM-snapshot': ({ id }) => <Vm id={id} />,
'VM-controller': ({ id }) => (
<span>
<Icon icon='host' /> <VmItem id={id} />
<Icon icon='host' /> <Vm id={id} />
</span>
),
@ -295,7 +425,7 @@ const xoItemToRender = {
// GPUs
vgpu: vgpu => <VgpuItem vgpu={vgpu} />,
vgpu: vgpu => <Vgpu vgpu={vgpu} />,
vgpuType: type => (
<span>
@ -318,9 +448,6 @@ const xoItemToRender = {
{backup.mode}
</span>{' '}
<span className='tag tag-warning'>{backup.remote.name}</span>{' '}
<span className='tag tag-success'>
{backup.jobName !== undefined ? backup.jobName : _('unknownJob')}
</span>{' '}
<FormattedDate
value={new Date(backup.timestamp)}
month='long'

View File

@ -247,11 +247,6 @@ class GenericSelect extends React.Component {
}
// GroupBy: Display option with margin if not disabled and containers exists.
/* TODO: When all item components are implemented, change type to this:
type: this.props.resourceSet !== undefined && option.xoItem.type !== undefined
? `${option.xoItem.type}-resourceSet`
: undefined
*/
_renderOption = option => (
<span
className={
@ -262,8 +257,9 @@ class GenericSelect extends React.Component {
>
{renderXoItem(option.xoItem, {
type:
this.props.resourceSet && option.xoItem.type === 'SR'
? 'SR-resourceSet'
this.props.resourceSet !== undefined &&
option.xoItem.type !== undefined
? `${option.xoItem.type}-resourceSet`
: undefined,
})}
</span>

View File

@ -16,7 +16,7 @@ import {
} from 'selectors'
import { forEach } from 'lodash'
import { SelectSr } from 'select-objects'
import { VmItem } from 'render-xo-item'
import { Vm } from 'render-xo-item'
import { ejectCd, isSrWritable, setDefaultSr } from 'xo'
@connectStore(
@ -60,7 +60,7 @@ export default class InstallPoolPatchesModalBody extends Component {
_getTooltip = createSelector(this._getVmsWithCds, vmIds =>
vmIds.map(vmId => (
<p className='m-0' key={vmId}>
<VmItem id={vmId} />
<Vm id={vmId} />
</p>
))
)

View File

@ -21,7 +21,7 @@ import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { Map } from 'immutable'
import { Number } from 'form'
import { renderXoItemFromId, RemoteItem } from 'render-xo-item'
import { renderXoItemFromId, Remote } from 'render-xo-item'
import { SelectRemote, SelectSr, SelectVm } from 'select-objects'
import {
addSubscriptions,
@ -762,7 +762,7 @@ export default decorate([
<Ul>
{map(state.remotes, (id, key) => (
<Li key={id}>
<RemoteItem id={id} />
<Remote id={id} />
<div className='pull-right'>
<DeleteOldBackupsFirst
handler={effects.setTargetDeleteFirst}

View File

@ -18,7 +18,7 @@ import { injectState, provideState } from 'reaclette'
import { isEmpty, groupBy, map, keyBy } from 'lodash'
import { subscribeBackupNgJobs, subscribeBackupNgLogs } from 'xo'
import { toggleState } from 'reaclette-utils'
import { VmItem, SrItem } from 'render-xo-item'
import { Vm, Sr } from 'render-xo-item'
import LogAlertBody from './log-alert-body'
import LogAlertHeader from './log-alert-header'
@ -195,7 +195,7 @@ const LOG_RESTORE_COLUMNS = [
name: _('labelVm'),
itemRenderer: ({ id, vm, status }) => (
<div>
{vm !== undefined && <VmItem id={vm.id} link newTab />}
{vm !== undefined && <Vm id={vm.id} link newTab />}
{vm === undefined && status === 'success' && (
<span className='text-warning'>{_('logsVmNotFound')}</span>
)}{' '}
@ -216,7 +216,7 @@ const LOG_RESTORE_COLUMNS = [
DURATION_COLUMN,
{
name: _('labelSr'),
itemRenderer: ({ data: { srId } }) => <SrItem id={srId} link newTab />,
itemRenderer: ({ data: { srId } }) => <Sr id={srId} link newTab />,
sortCriteria: ({ data: { srId } }, { srs }) =>
get(() => srs[srId].name_label),
},

View File

@ -10,7 +10,7 @@ import { countBy, filter, get, keyBy, map } from 'lodash'
import { FormattedDate } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { runBackupNgJob, subscribeBackupNgLogs, subscribeRemotes } from 'xo'
import { VmItem, SrItem, RemoteItem } from 'render-xo-item'
import { Vm, Sr, Remote } from 'render-xo-item'
const TASK_STATUS = {
failure: {
@ -195,7 +195,7 @@ export default decorate([
let globalIsFull
return (
<li key={taskLog.data.id} className='list-group-item'>
<VmItem id={taskLog.data.id} link newTab /> (
<Vm id={taskLog.data.id} link newTab /> (
{taskLog.data.id.slice(4, 8)}){' '}
<TaskStateInfos status={taskLog.status} />{' '}
{scheduleId !== undefined &&
@ -231,12 +231,12 @@ export default decorate([
</span>
) : subTaskLog.data.type === 'remote' ? (
<span>
<RemoteItem id={subTaskLog.data.id} link newTab /> (
<Remote id={subTaskLog.data.id} link newTab /> (
{subTaskLog.data.id.slice(4, 8)})
</span>
) : (
<span>
<SrItem id={subTaskLog.data.id} link newTab /> (
<Sr id={subTaskLog.data.id} link newTab /> (
{subTaskLog.data.id.slice(4, 8)})
</span>
)}{' '}

View File

@ -18,7 +18,7 @@ import {
some,
toArray,
} from 'lodash'
import { PoolItem } from 'render-xo-item'
import { Pool } from 'render-xo-item'
import {
createFilter,
createGetObject,
@ -73,7 +73,7 @@ export class TaskItem extends Component {
const COLUMNS = [
{
default: true,
itemRenderer: ({ $poolId }) => <PoolItem id={$poolId} link />,
itemRenderer: ({ $poolId }) => <Pool id={$poolId} link />,
name: _('pool'),
sortCriteria: (task, userData) => {
const pool = userData.pools[task.$poolId]
@ -101,7 +101,7 @@ const COLUMNS = [
const FINISHED_TASKS_COLUMNS = [
{
itemRenderer: ({ $poolId }) => <PoolItem id={$poolId} link />,
itemRenderer: ({ $poolId }) => <Pool id={$poolId} link />,
name: _('pool'),
},
{

View File

@ -3,7 +3,7 @@ import ActionButton from 'action-button'
import Component from 'base-component'
import Link from 'link'
import React from 'react'
import renderXoItem, { PoolItem } from 'render-xo-item'
import renderXoItem, { Pool } from 'render-xo-item'
import SortedTable from 'sorted-table'
import { connectStore } from 'utils'
import { createSelector, createGetObjectsOfType, createFilter } from 'selectors'
@ -69,7 +69,7 @@ const XOSAN_COLUMNS = [
},
{
name: _('xosanPool'),
itemRenderer: sr => <PoolItem id={sr.$pool} link />,
itemRenderer: sr => <Pool id={sr.$pool} link />,
},
{
name: _('xosanLicense'),