feat(xo-server, xo-web): expose and edit custom fields (#5387)

See #4730
This commit is contained in:
Rajaa.BARHTAOUI 2020-11-27 13:39:08 +01:00 committed by GitHub
parent 00e53f455b
commit 7961ff0785
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 227 additions and 0 deletions

View File

@ -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

View 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'],
}

View File

@ -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',

View 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 }

View File

@ -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',

View File

@ -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) })

View File

@ -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 />

View File

@ -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>

View File

@ -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>

View File

@ -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 />