feat(xo-server, xo-web): expose and edit custom fields (#5387)
See #4730
This commit is contained in:
parent
00e53f455b
commit
7961ff0785
@ -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
|
||||
|
||||
|
52
packages/xo-server/src/api/custom-field.js
Normal file
52
packages/xo-server/src/api/custom-field.js
Normal file
@ -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'],
|
||||
}
|
@ -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',
|
||||
|
129
packages/xo-web/src/common/custom-fields.js
Normal file
129
packages/xo-web/src/common/custom-fields.js
Normal file
@ -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 }) => (
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('name')}</Col>
|
||||
<Col size={6}>
|
||||
<input
|
||||
autoFocus
|
||||
className='form-control'
|
||||
name='name'
|
||||
onChange={effects.onChange}
|
||||
required
|
||||
type='text'
|
||||
value={value.name}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('value')}</Col>
|
||||
<Col size={6}>
|
||||
<input
|
||||
className='form-control'
|
||||
name='value'
|
||||
onChange={effects.onChange}
|
||||
required
|
||||
type='text'
|
||||
value={value.value}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
|
||||
const CustomFields = decorate([
|
||||
connectStore({
|
||||
object: createGetObject((_, props) => props.object),
|
||||
}),
|
||||
provideState({
|
||||
effects: {
|
||||
addCustomField: () => (state, { object: { id } }) =>
|
||||
form({
|
||||
render: props => <AddCustomFieldModal {...props} />,
|
||||
defaultValue: { name: '', value: '' },
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='add' /> {_('addCustomField')}
|
||||
</span>
|
||||
),
|
||||
}).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 (
|
||||
<div>
|
||||
{customFields.map(([key, value]) => {
|
||||
const name = key.substring(CUSTOM_FIELDS_KEY_PREFIX.length)
|
||||
return (
|
||||
<div key={key}>
|
||||
{name}: <Text data-name={name} value={value} onChange={effects.setCustomFieldValue} />
|
||||
<Tooltip content={_('deleteCustomField')}>
|
||||
<a data-name={name} onClick={effects.removeCustomField} role='button'>
|
||||
<Icon icon='remove' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={effects.addCustomField}
|
||||
icon='add'
|
||||
size='small'
|
||||
tooltip={_('addCustomField')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
])
|
||||
|
||||
CustomFields.propTypes = {
|
||||
object: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export { CustomFields }
|
@ -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',
|
||||
|
@ -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) })
|
||||
|
@ -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 {
|
||||
<Text value={host.logging.syslog_destination || ''} onChange={this._setRemoteSyslogHost} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('customFields')}</th>
|
||||
<td>
|
||||
<CustomFields object={host.id} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
|
@ -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 {
|
||||
<PoolMaster pool={pool} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('customFields')}</th>
|
||||
<td>
|
||||
<CustomFields object={pool.id} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('syslogRemoteHost')}</th>
|
||||
<td>
|
||||
|
@ -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 }) => (
|
||||
<th>{_('provisioning')}</th>
|
||||
<td>{defined(sr.allocationStrategy, _('unknown'))}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('customFields')}</th>
|
||||
<td>
|
||||
<CustomFields object={sr.id} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Col>
|
||||
|
@ -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 {
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<th>{_('customFields')}</th>
|
||||
<td>
|
||||
<CustomFields object={vm.id} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
|
Loading…
Reference in New Issue
Block a user