diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc21701ac..1b2396d42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
- [Backup NG form] Add offline snapshot info (PR [#3144](https://github.com/vatesfr/xen-orchestra/pull/3144))
- [Backup NG overview] Display concurrency and offline snapshot value [3087](https://github.com/vatesfr/xen-orchestra/issues/3087) (PR [3145](https://github.com/vatesfr/xen-orchestra/pull/3145))
- [VM revert] notify the result of reverting a VM [3095](https://github.com/vatesfr/xen-orchestra/issues/3095) (PR [3150](https://github.com/vatesfr/xen-orchestra/pull/3150))
+- [Backup NG logs] Link XO items in the details modal [#2711](https://github.com/vatesfr/xen-orchestra/issues/2711) (PR [#3171](https://github.com/vatesfr/xen-orchestra/pull/3171))
### Bug fixes
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index a2809f66f..b76fdd878 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -12,6 +12,7 @@ const messages = {
statusLoading: 'Loading…',
errorPageNotFound: 'Page not found',
errorNoSuchItem: 'no such item',
+ errorUnknownItem: 'unknown item',
editableLongClickPlaceholder: 'Long click to edit',
editableClickPlaceholder: 'Click to edit',
diff --git a/packages/xo-web/src/common/render-xo-item.js b/packages/xo-web/src/common/render-xo-item.js
index 82c0a54f6..7427de351 100644
--- a/packages/xo-web/src/common/render-xo-item.js
+++ b/packages/xo-web/src/common/render-xo-item.js
@@ -1,13 +1,16 @@
import _ from 'intl'
+import PropTypes from 'prop-types'
import React from 'react'
import { startsWith } from 'lodash'
import Icon from './icon'
+import Link from './link'
import propTypes from './prop-types-decorator'
-import { createGetObject } from './selectors'
+import { addSubscriptions, connectStore, formatSize } from './utils'
+import { createGetObject, createSelector } from './selectors'
import { FormattedDate } from 'react-intl'
-import { isSrWritable } from './xo'
-import { connectStore, formatSize } from './utils'
+import { get } from './xo-defined'
+import { isSrWritable, subscribeRemotes } from './xo'
// ===================================================================
@@ -17,6 +20,112 @@ const OBJECT_TYPE_TO_ICON = {
network: 'network',
}
+const COMMON_PROP_TYPES = {
+ link: PropTypes.bool,
+}
+
+const XoItem = ({ children, item, link, to }) =>
+ item !== undefined ? (
+ link ? (
+
+ {children()}
+
+ ) : (
+ children()
+ )
+ ) : (
+ {_('errorUnknownItem')}
+ )
+
+XoItem.propTypes = {
+ ...COMMON_PROP_TYPES,
+ item: PropTypes.object,
+ to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+}
+// ===================================================================
+
+const XO_ITEM_PROP_TYPES = {
+ ...COMMON_PROP_TYPES,
+ id: PropTypes.string.isRequired,
+}
+
+export const VmItem = [
+ connectStore(() => {
+ const getVm = createGetObject()
+ return {
+ vm: getVm,
+ container: createGetObject(
+ createSelector(getVm, vm => get(() => vm.$container))
+ ),
+ }
+ }),
+ ({ vm, container, ...props }) => (
+ vm.id)}`} {...props}>
+ {() => (
+
+ {' '}
+ {vm.name_label || vm.id}
+ {container !== undefined &&
+ ` (${container.name_label || container.id})`}
+
+ )}
+
+ ),
+].reduceRight((value, decorator) => decorator(value))
+
+VmItem.propTypes = XO_ITEM_PROP_TYPES
+
+export const SrItem = [
+ connectStore(() => {
+ const getSr = createGetObject()
+ return {
+ sr: getSr,
+ container: createGetObject(
+ createSelector(getSr, sr => get(() => sr.$container))
+ ),
+ }
+ }),
+ ({ sr, container, ...props }) => (
+ sr.id)}`} {...props}>
+ {() => (
+
+ {sr.name_label || sr.id}
+ {container !== undefined && (
+ - {container.name_label}
+ )}
+ {isSrWritable(sr) && (
+ {` (${formatSize(sr.size - sr.physical_usage)} free)`}
+ )}
+
+ )}
+
+ ),
+].reduceRight((value, decorator) => decorator(value))
+
+SrItem.propTypes = XO_ITEM_PROP_TYPES
+
+export const RemoteItem = [
+ addSubscriptions(({ id }) => ({
+ remote: cb =>
+ subscribeRemotes(remotes => {
+ cb(get(() => remotes.find(remote => remote.id === id)))
+ }),
+ })),
+ ({ remote, ...props }) => (
+
+ {() => (
+
+ {remote.name}
+
+ )}
+
+ ),
+].reduceRight((value, decorator) => decorator(value))
+
+RemoteItem.propTypes = XO_ITEM_PROP_TYPES
+
+// ===================================================================
+
// Host, Network, VM-template.
const PoolObjectItem = propTypes({
object: propTypes.object.isRequired,
@@ -40,48 +149,6 @@ const PoolObjectItem = propTypes({
})
)
-// SR.
-const SrItem = propTypes({
- sr: propTypes.object.isRequired,
-})(
- connectStore(() => {
- const getContainer = createGetObject((_, props) => props.sr.$container)
-
- return (state, props) => ({
- container: getContainer(state, props),
- })
- })(({ sr, container }) => (
-
- {sr.name_label || sr.id}
- {container !== undefined && (
- - {container.name_label}
- )}
- {isSrWritable(sr) && (
- {` (${formatSize(sr.size - sr.physical_usage)} free)`}
- )}
-
- ))
-)
-
-// VM.
-const VmItem = propTypes({
- vm: propTypes.object.isRequired,
-})(
- connectStore(() => {
- const getContainer = createGetObject((_, props) => props.vm.$container)
-
- return (state, props) => ({
- container: getContainer(state, props),
- })
- })(({ vm, container }) => (
-
- {' '}
- {vm.name_label || vm.id}
- {container && ` (${container.name_label || container.id})`}
-
- ))
-)
-
const VgpuItem = connectStore(() => ({
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
}))(({ vgpu, vgpuType }) => (
@@ -104,11 +171,7 @@ const xoItemToRender = {
{group.name}
),
- remote: remote => (
-
- {remote.value.name}
-
- ),
+ remote: ({ value: { id } }) => ,
role: role => {role.name},
user: user => (
@@ -160,14 +223,14 @@ const xoItemToRender = {
network: network => ,
// SR.
- SR: sr => ,
+ SR: ({ id }) => ,
// VM.
- VM: vm => ,
- 'VM-snapshot': vm => ,
- 'VM-controller': vm => (
+ VM: ({ id }) => ,
+ 'VM-snapshot': ({ id }) => ,
+ 'VM-controller': ({ id }) => (
-
+
),
diff --git a/packages/xo-web/src/xo-app/logs/log-alert-body.js b/packages/xo-web/src/xo-app/logs/log-alert-body.js
index 216a890b3..013c09270 100644
--- a/packages/xo-web/src/xo-app/logs/log-alert-body.js
+++ b/packages/xo-web/src/xo-app/logs/log-alert-body.js
@@ -2,7 +2,6 @@ import _, { FormattedDuration } from 'intl'
import ActionButton from 'action-button'
import Icon from 'icon'
import React from 'react'
-import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
import Select from 'form/select'
import Tooltip from 'tooltip'
import { addSubscriptions, formatSize, formatSpeed } from 'utils'
@@ -10,6 +9,7 @@ import { countBy, filter, get, keyBy, map } from 'lodash'
import { FormattedDate } from 'react-intl'
import { injectState, provideState } from '@julien-f/freactal'
import { runBackupNgJob, subscribeBackupNgLogs, subscribeRemotes } from 'xo'
+import { VmItem, SrItem, RemoteItem } from 'render-xo-item'
const TASK_STATUS = {
failure: {
@@ -166,7 +166,7 @@ export default [
let globalIsFull
return (
- {renderXoItemFromId(taskLog.data.id)} ({taskLog.data.id.slice(
+ ({taskLog.data.id.slice(
4,
8
)}) {' '}
@@ -202,17 +202,14 @@ export default [
) : subTaskLog.data.type === 'remote' ? (
- {get(remotes, subTaskLog.data.id) !== undefined
- ? renderXoItem({
- type: 'remote',
- value: remotes[subTaskLog.data.id],
- })
- : _('errorNoSuchItem')}{' '}
- ({subTaskLog.data.id.slice(4, 8)})
+ ({subTaskLog.data.id.slice(
+ 4,
+ 8
+ )})
) : (
- {renderXoItemFromId(subTaskLog.data.id)} ({subTaskLog.data.id.slice(
+ ({subTaskLog.data.id.slice(
4,
8
)})