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 ( +
+ {name}: + + + + + +
+ ) + })} +
+ +
+
+ ) + }, +]) + +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')} + + + +