diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index ea7caf3e5..2eb042c97 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -19,6 +19,7 @@
- [Plugin/load-balancer] Limit concurrent VM migrations to 2 (configurable) to avoid long paused VMs [#7084](https://github.com/vatesfr/xen-orchestra/issues/7084) (PR [#7297](https://github.com/vatesfr/xen-orchestra/pull/7297))
- [Tags] Admin can create colored tags (PR [#7262](https://github.com/vatesfr/xen-orchestra/pull/7262))
- [Tags] Add tooltips on `xo:no-bak` and `xo:notify-on-snapshot` tags (PR [#7335](https://github.com/vatesfr/xen-orchestra/pull/7335))
+- [VM] Custom notes [#5792](https://github.com/vatesfr/xen-orchestra/issues/5792) (PR [#7322](https://github.com/vatesfr/xen-orchestra/pull/7322))
### Bug fixes
diff --git a/packages/xo-server/src/api/vm.mjs b/packages/xo-server/src/api/vm.mjs
index 5b480a07f..d5c5b6ff5 100644
--- a/packages/xo-server/src/api/vm.mjs
+++ b/packages/xo-server/src/api/vm.mjs
@@ -694,6 +694,8 @@ set.params = {
name_description: { type: 'string', minLength: 0, optional: true },
+ notes: { type: ['string', 'null'], maxLength: 2048, optional: true },
+
high_availability: {
optional: true,
enum: getHaValues(),
diff --git a/packages/xo-server/src/xapi-object-to-xo.mjs b/packages/xo-server/src/xapi-object-to-xo.mjs
index 0f1a01d69..8e371d786 100644
--- a/packages/xo-server/src/xapi-object-to-xo.mjs
+++ b/packages/xo-server/src/xapi-object-to-xo.mjs
@@ -401,6 +401,7 @@ const TRANSFORMS = {
installTime: metrics && toTimestamp(metrics.install_time),
name_description: obj.name_description,
name_label: obj.name_label,
+ notes: otherConfig['xo:notes'],
other: otherConfig,
os_version: (guestMetrics && guestMetrics.os_version) || null,
parent: link(obj, 'parent'),
diff --git a/packages/xo-server/src/xapi/mixins/vm.mjs b/packages/xo-server/src/xapi/mixins/vm.mjs
index 20405c439..fa35291d7 100644
--- a/packages/xo-server/src/xapi/mixins/vm.mjs
+++ b/packages/xo-server/src/xapi/mixins/vm.mjs
@@ -382,6 +382,11 @@ const methods = {
nameLabel: true,
+ notes: {
+ get: vm => vm.other_config['xo:notes'],
+ set: (value, vm) => vm.update_other_config('xo:notes', value),
+ },
+
PV_args: true,
tags: true,
diff --git a/packages/xo-web/package.json b/packages/xo-web/package.json
index 0712dd204..941342947 100644
--- a/packages/xo-web/package.json
+++ b/packages/xo-web/package.json
@@ -123,6 +123,7 @@
"relative-luminance": "^2.0.1",
"reselect": "^2.5.4",
"rimraf": "^5.0.1",
+ "sanitize-html": "^2.11.0",
"sass": "^1.38.1",
"semver": "^6.0.0",
"strip-ansi": "^5.2.0",
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index ea466f242..f116edacf 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -1197,6 +1197,9 @@ const messages = {
'Enabling this will allow the VM to automatically install Citrix PV drivers from Windows Update. This only includes drivers, the Citrix management agent must still be separately installed.',
windowsToolsModalWarning:
'If you have previously installed XCP-ng tools instead of Citrix tools, this option will break your VM.',
+ editVmNotes: 'Edit VM notes',
+ supportsMarkdown: 'Supports Markdown syntax',
+ vmNotesTooLong: 'VM notes cannot be longer than 2048 characters',
// ----- VM stat tab -----
statsCpu: 'CPU usage',
diff --git a/packages/xo-web/src/common/xo/edit-vm-notes-modal/index.js b/packages/xo-web/src/common/xo/edit-vm-notes-modal/index.js
new file mode 100644
index 000000000..f01e8bae2
--- /dev/null
+++ b/packages/xo-web/src/common/xo/edit-vm-notes-modal/index.js
@@ -0,0 +1,36 @@
+import _ from 'intl'
+import Icon from 'icon'
+import clamp from 'lodash/clamp'
+import Component from 'base-component'
+import React from 'react'
+
+export default class EditVmNotesModalBody extends Component {
+ get value() {
+ return { notes: this.state.notes ?? this.props.vm.notes ?? '' }
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
index d4086a2f0..32bfb8294 100644
--- a/packages/xo-web/src/common/xo/index.js
+++ b/packages/xo-web/src/common/xo/index.js
@@ -1524,6 +1524,18 @@ export const changeVirtualizationMode = vm =>
})
)
+import EditVmNotesModalBody from './edit-vm-notes-modal' // eslint-disable-line import/first
+export const editVmNotes = async vm => {
+ const { notes } = await confirm({
+ icon: 'edit',
+ title: _('editVmNotes'),
+ body: ,
+ })
+
+ // Remove notes if `''` is passed
+ await _call('vm.set', { id: resolveId(vm), notes: notes || null })
+}
+
export const createKubernetesCluster = params => _call('xoa.recipe.createKubernetesCluster', params)
export const deleteTemplates = templates =>
diff --git a/packages/xo-web/src/xo-app/vm/tab-general.js b/packages/xo-web/src/xo-app/vm/tab-general.js
index ca860c753..ffd35ac3e 100644
--- a/packages/xo-web/src/xo-app/vm/tab-general.js
+++ b/packages/xo-web/src/xo-app/vm/tab-general.js
@@ -1,15 +1,18 @@
import _ from 'intl'
+import ActionButton from 'action-button'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
import defined, { get } from '@xen-orchestra/defined'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
+import marked from 'marked'
import React from 'react'
import HomeTags from 'home-tags'
import renderXoItem, { VmTemplate } from 'render-xo-item'
+import sanitizeHtml from 'sanitize-html'
import Tooltip from 'tooltip'
-import { addTag, editVm, removeTag, subscribeUsers } from 'xo'
+import { addTag, editVm, editVmNotes, removeTag, subscribeUsers } from 'xo'
import { BlockLink } from 'link'
import { FormattedRelative, FormattedDate } from 'react-intl'
import { Container, Row, Col } from 'grid'
@@ -31,6 +34,18 @@ const CREATED_VM_STYLES = {
whiteSpace: 'pre-line',
}
+const NOTES_STYLE = {
+ maxWidth: '70%',
+ margin: 'auto',
+ border: 'dashed 1px #999',
+ padding: '1em',
+ borderRadius: '10px',
+}
+
+const SANITIZE_OPTIONS = {
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
+}
+
const GuestToolsDetection = ({ vm }) => {
if (vm.power_state !== 'Running' || vm.pvDriversDetected === undefined) {
return null
@@ -276,6 +291,20 @@ const GeneralTab = decorate([
)}
+
+
+ {vm.notes !== undefined && (
+
+ )}
+
+ {_('editVmNotes')}
+
+
+
)
},
diff --git a/yarn.lock b/yarn.lock
index 9711d72f2..18015bec4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11876,7 +11876,7 @@ htmlparser2@^6.1.0:
domutils "^2.5.2"
entities "^2.0.0"
-htmlparser2@^8.0.1:
+htmlparser2@^8.0.0, htmlparser2@^8.0.1:
version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
@@ -16395,6 +16395,11 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
+parse-srcset@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
+ integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
+
parse5-htmlparser2-tree-adapter@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
@@ -17179,7 +17184,7 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2
picocolors "^0.2.1"
source-map "^0.6.1"
-postcss@^8.4.14, postcss@^8.4.32, postcss@^8.4.33:
+postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.32, postcss@^8.4.33:
version "8.4.33"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
@@ -18951,6 +18956,18 @@ safe-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+sanitize-html@^2.11.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.11.0.tgz#9a6434ee8fcaeddc740d8ae7cd5dd71d3981f8f6"
+ integrity sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==
+ dependencies:
+ deepmerge "^4.2.2"
+ escape-string-regexp "^4.0.0"
+ htmlparser2 "^8.0.0"
+ is-plain-object "^5.0.0"
+ parse-srcset "^1.0.2"
+ postcss "^8.3.11"
+
sasl-anonymous@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/sasl-anonymous/-/sasl-anonymous-0.1.0.tgz#f544c7e824df2a40d9ad4733829572cc8d9ed5a5"