diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 00b6ae5ff..d2f55cf9c 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -15,6 +15,7 @@
- [Proxy] Ability to restore a file from VM backup (PR [#5359](https://github.com/vatesfr/xen-orchestra/pull/5359))
- [Web Hooks] `backupNg.runJob` is now triggered by scheduled runs [#5205](https://github.com/vatesfr/xen-orchestra/issues/5205) (PR [#5360](https://github.com/vatesfr/xen-orchestra/pull/5360))
- [Licensing] Add trial end information banner (PR [#5374](https://github.com/vatesfr/xen-orchestra/pull/5374))
+- Assign custom fields on pools, hosts, SRs, and VMs in advanced tab [#4730](https://github.com/vatesfr/xen-orchestra/issues/4730) (PR [#5387](https://github.com/vatesfr/xen-orchestra/pull/5387))
### Bug fixes
diff --git a/packages/xo-server/src/api/custom-field.js b/packages/xo-server/src/api/custom-field.js
new file mode 100644
index 000000000..25ac05337
--- /dev/null
+++ b/packages/xo-server/src/api/custom-field.js
@@ -0,0 +1,52 @@
+const KEY_PREFIX = 'XenCenter.CustomFields.'
+
+export async function add({ object, name, value }) {
+ await this.getXapiObject(object).$call('add_to_other_config', KEY_PREFIX + name, value)
+}
+
+add.description = 'Add a new custom field to an object'
+
+add.params = {
+ id: { type: 'string' },
+ name: { type: 'string' },
+ value: { type: 'string' },
+}
+
+add.resolve = {
+ object: ['id', null, 'administrate'],
+}
+
+// -------------------------------------------------------------------
+
+export async function remove({ object, name }) {
+ await this.getXapiObject(object).update_other_config(KEY_PREFIX + name, null)
+}
+
+remove.description = 'Remove an existing custom field from an object'
+
+remove.params = {
+ id: { type: 'string' },
+ name: { type: 'string' },
+}
+
+remove.resolve = {
+ object: ['id', null, 'administrate'],
+}
+
+// -------------------------------------------------------------------
+
+export async function set({ object, name, value }) {
+ await this.getXapiObject(object).update_other_config(KEY_PREFIX + name, value)
+}
+
+set.description = 'Set a custom field'
+
+set.params = {
+ id: { type: 'string' },
+ name: { type: 'string' },
+ value: { type: 'string' },
+}
+
+set.resolve = {
+ object: ['id', null, 'administrate'],
+}
diff --git a/packages/xo-server/src/xapi-object-to-xo.js b/packages/xo-server/src/xapi-object-to-xo.js
index 39f3d645b..c92cb0a0b 100644
--- a/packages/xo-server/src/xapi-object-to-xo.js
+++ b/packages/xo-server/src/xapi-object-to-xo.js
@@ -110,6 +110,7 @@ const TRANSFORMS = {
name_description: obj.name_description,
name_label: obj.name_label || obj.$master.name_label,
xosanPackInstallationTime: toTimestamp(obj.other_config.xosan_pack_installation_time),
+ otherConfig: obj.other_config,
cpus: {
cores: cpuInfo && +cpuInfo.cpu_count,
sockets: cpuInfo && +cpuInfo.socket_count,
@@ -209,6 +210,7 @@ const TRANSFORMS = {
}
})(),
multipathing: otherConfig.multipathing === 'true',
+ otherConfig,
patches: link(obj, 'patches'),
powerOnMode: obj.power_on_mode,
power_state: metrics ? (isRunning ? 'Running' : 'Halted') : 'Unknown',
diff --git a/packages/xo-web/src/common/custom-fields.js b/packages/xo-web/src/common/custom-fields.js
new file mode 100644
index 000000000..30bc6af77
--- /dev/null
+++ b/packages/xo-web/src/common/custom-fields.js
@@ -0,0 +1,129 @@
+import _ from 'intl'
+import defined from '@xen-orchestra/defined'
+import React from 'react'
+import PropTypes from 'prop-types'
+import { injectState, provideState } from 'reaclette'
+
+import ActionButton from './action-button'
+import decorate from './apply-decorators'
+import Icon from './icon'
+import SingleLineRow from './single-line-row'
+import Tooltip from './tooltip'
+import { addCustomField, removeCustomField, setCustomField } from './xo'
+import { connectStore } from './utils'
+import { Container, Col } from './grid'
+import { createGetObject } from './selectors'
+import { form } from './modal'
+import { Text } from './editable'
+
+const CUSTOM_FIELDS_KEY_PREFIX = 'XenCenter.CustomFields.'
+
+const AddCustomFieldModal = decorate([
+ provideState({
+ effects: {
+ onChange(_, { target: { name, value } }) {
+ const { props } = this
+ props.onChange({
+ ...props.value,
+ [name]: value,
+ })
+ },
+ },
+ }),
+ injectState,
+ ({ effects, value }) => (
+
+
+ {_('name')}
+
+
+
+
+
+ {_('value')}
+
+
+
+
+
+ ),
+])
+
+const CustomFields = decorate([
+ connectStore({
+ object: createGetObject((_, props) => props.object),
+ }),
+ provideState({
+ effects: {
+ addCustomField: () => (state, { object: { id } }) =>
+ form({
+ render: props => ,
+ defaultValue: { name: '', value: '' },
+ header: (
+
+ {_('addCustomField')}
+
+ ),
+ }).then(({ name, value }) => addCustomField(id, name.trim(), value.trim())),
+ removeCustomField: (_, { currentTarget: { dataset } }) => (_, { object: { id } }) =>
+ removeCustomField(id, dataset.name),
+ setCustomFieldValue: (_, value, { name }) => (_, { object: { id } }) => setCustomField(id, name, value),
+ },
+ computed: {
+ customFields: (_, { object }) =>
+ Object.entries(defined(object.otherConfig, object.other_config, object.other, {}))
+ .filter(([key]) => key.startsWith(CUSTOM_FIELDS_KEY_PREFIX))
+ .sort(([keyA], [keyB]) => (keyA < keyB ? -1 : 1)),
+ },
+ }),
+ injectState,
+ ({ effects, state: { customFields = [] } }) => {
+ return (
+
+ {customFields.map(([key, value]) => {
+ const name = key.substring(CUSTOM_FIELDS_KEY_PREFIX.length)
+ return (
+
+ )
+ })}
+
+
+ )
+ },
+])
+
+CustomFields.propTypes = {
+ object: PropTypes.string.isRequired,
+}
+
+export { CustomFields }
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index da0de3a5c..894a7ed6d 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -61,6 +61,7 @@ const messages = {
proxy: 'Proxy',
proxies: 'Proxies',
name: 'Name',
+ value: 'Value',
address: 'Address',
vm: 'VM',
destinationSR: 'Destination SR',
@@ -94,6 +95,9 @@ const messages = {
privateKey: 'Private key (PKCS#8)',
installNewCertificate: 'Install new certificate',
replaceExistingCertificate: 'Replace existing certificate',
+ customFields: 'Custom fields',
+ addCustomField: 'Add custom field',
+ deleteCustomField: 'Delete custom field',
// ----- Modals -----
alertOk: 'OK',
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
index 1e6200a19..f00824e9e 100644
--- a/packages/xo-web/src/common/xo/index.js
+++ b/packages/xo-web/src/common/xo/index.js
@@ -1797,6 +1797,17 @@ export const addTag = (object, tag) => _call('tag.add', { id: resolveId(object),
export const removeTag = (object, tag) => _call('tag.remove', { id: resolveId(object), tag })
+// Custom fields ------------------------------------------------------------------------
+
+export const addCustomField = (id, name, value) =>
+ _call('customField.add', { id, name, value })
+
+export const removeCustomField = (id, name) =>
+ _call('customField.remove', { id, name })
+
+export const setCustomField = (id, name, value) =>
+ _call('customField.set', { id, name, value })
+
// Tasks --------------------------------------------------------------
export const cancelTask = task => _call('task.cancel', { id: resolveId(task) })
diff --git a/packages/xo-web/src/xo-app/host/tab-advanced.js b/packages/xo-web/src/xo-app/host/tab-advanced.js
index 127070a47..efd1abd3a 100644
--- a/packages/xo-web/src/xo-app/host/tab-advanced.js
+++ b/packages/xo-web/src/xo-app/host/tab-advanced.js
@@ -14,6 +14,7 @@ import Upgrade from 'xoa-upgrade'
import { addSubscriptions, compareVersions, connectStore, getIscsiPaths } from 'utils'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
+import { CustomFields } from 'custom-fields'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { forEach, isEmpty, map, noop } from 'lodash'
import { FormattedRelative, FormattedTime } from 'react-intl'
@@ -359,6 +360,12 @@ export default class extends Component {
+
+ {_('customFields')} |
+
+
+ |
+
diff --git a/packages/xo-web/src/xo-app/pool/tab-advanced.js b/packages/xo-web/src/xo-app/pool/tab-advanced.js
index bb821a4be..0e91e5628 100644
--- a/packages/xo-web/src/xo-app/pool/tab-advanced.js
+++ b/packages/xo-web/src/xo-app/pool/tab-advanced.js
@@ -9,6 +9,7 @@ import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
+import { CustomFields } from 'custom-fields'
import { createGetObjectsOfType, createGroupBy } from 'selectors'
import { injectIntl } from 'react-intl'
import { map } from 'lodash'
@@ -84,6 +85,12 @@ export default class TabAdvanced extends Component {
+
+ {_('customFields')} |
+
+
+ |
+
{_('syslogRemoteHost')} |
diff --git a/packages/xo-web/src/xo-app/sr/tab-advanced.js b/packages/xo-web/src/xo-app/sr/tab-advanced.js
index edf3b4ab1..dff6502b3 100644
--- a/packages/xo-web/src/xo-app/sr/tab-advanced.js
+++ b/packages/xo-web/src/xo-app/sr/tab-advanced.js
@@ -6,6 +6,7 @@ import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import { addSubscriptions, connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
+import { CustomFields } from 'custom-fields'
import { createGetObjectsOfType } from 'selectors'
import { createSelector } from 'reselect'
import { createSrUnhealthyVdiChainsLengthSubscription, deleteSr } from 'xo'
@@ -68,6 +69,12 @@ export default ({ sr }) => (
| {_('provisioning')} |
{defined(sr.allocationStrategy, _('unknown'))} |
+
+ {_('customFields')} |
+
+
+ |
+
diff --git a/packages/xo-web/src/xo-app/vm/tab-advanced.js b/packages/xo-web/src/xo-app/vm/tab-advanced.js
index 3d2baec83..63425e3e0 100644
--- a/packages/xo-web/src/xo-app/vm/tab-advanced.js
+++ b/packages/xo-web/src/xo-app/vm/tab-advanced.js
@@ -15,6 +15,7 @@ import Tooltip from 'tooltip'
import { error } from 'notification'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
+import { CustomFields } from 'custom-fields'
import { injectState, provideState } from 'reaclette'
import { Number, Select as EditableSelect, Size, Text, XoSelect } from 'editable'
import { Select, Toggle } from 'form'
@@ -771,6 +772,12 @@ export default class TabAdvanced extends Component {
)}
+
+ {_('customFields')} |
+
+
+ |
+