Compare commits

...

106 Commits

Author SHA1 Message Date
Julien Fontanet
7da0146d3e feat(xo-server/token): savelog last use info 2023-10-27 11:37:44 +02:00
Julien Fontanet
f3bbcbde08 feat(xo-cli): only create a single token per instance (and user) 2023-10-27 11:28:39 +02:00
Julien Fontanet
0559fe8649 feat(xo-server/token): client info support 2023-10-27 11:28:39 +02:00
Julien Fontanet
9e70397240 fix(xo-server/redis): fix indexes handling
Introduced by 225a67ae3
2023-10-27 11:27:25 +02:00
Thierry Goettelmann
5f69b0e9a0 feat(lite/console): new console toolbar (#7088) 2023-10-27 10:27:51 +02:00
Julien Fontanet
2a9bff1607 chore(xo-server/importConfig): don't use deptree 2023-10-27 10:14:02 +02:00
Pierre Donias
9e621d7de8 feat(lite/header): replace logo with "XO LITE" (#7118) 2023-10-27 09:16:28 +02:00
Mathieu
3e5c73528d feat(xo-server,xo-web/XOSTOR): XOSTOR implementation (#6983)
See https://xcp-ng.org/forum/topic/5361
2023-10-26 16:58:59 +02:00
Pierre Donias
397b5cd56d fix(xo-server/snapshot): allow self user that is member of a group to snapshot (#7129)
Introduced by a88798cc22
See Zammad#18478
2023-10-26 16:08:43 +02:00
Julien Fontanet
55cb6042e8 chore(yarn.lock): update dev deps 2023-10-26 11:00:14 +02:00
Pierre Donias
339d920b78 feat(xo-web/proxy): ability to open support tunnel on XO Proxy (#7127)
Requires #7126
2023-10-25 17:26:06 +02:00
Julien Fontanet
f14f716f3d feat(xo-server/api): proxy.openSupportTunnel (#7126)
The goal is to provide an easier way for the support team to open a tunnel on a proxy appliance.

This is the server side of this feature.
2023-10-25 17:12:17 +02:00
Julien Fontanet
fb83d1fc98 feat(xo-server/api): ignorable parameters (#7125) 2023-10-25 15:49:41 +02:00
Julien Fontanet
62208e7847 fix(xo-server-transport-xmpp): fix loading (#7082)
Fixes https://xcp-ng.org/forum/post/66402

Introduced by d6fc86b6b
2023-10-25 14:36:40 +02:00
Julien Fontanet
df91772f5c chore(xo-server/server): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
cf8a9d40be chore(xo-server/remote): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
93d1c6c3fc chore(xo-server/plugin-metadata): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
f1fa811e5c chore(xo-server/user): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
5a9812c492 chore(xo-server/group): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
b53d613a64 chore(xo-server/token): use builtin unserialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
225a67ae3b chore(xo-server/redis): proper (un)serialization support 2023-10-25 11:48:53 +02:00
Mathieu
c7eb7db463 feat(xo-web/about): display if XO from source is up to date (#7091)
Fixes #5934
2023-10-24 17:14:01 +02:00
Pierre Donias
edfa729672 chore(lite/assets): remove darkreader properties in SVG files (#7121) 2023-10-24 16:40:37 +02:00
Mathieu
77d9798319 fix(xo-web/vtpm): fix various an error has occured (#7122)
Introduced by 8834af65f7
Introduced by 1a1dd0531d

Fix `an error has occurred` in the VM advanced tab and on the VM creation form
if the user does not have pool permission.
2023-10-24 16:26:36 +02:00
Pierre Donias
680f1e2f07 chore(lite): serve Poppins font internally (#7117) 2023-10-24 15:19:54 +02:00
Julien Fontanet
7c009b0fc0 feat(xo-server): support reading JSON records in Redis
This allows forward compatibility with future versions which will use JSON records in the future.
2023-10-23 15:13:28 +02:00
Pierre Donias
eb7de4f2dd feat(xo-web/self): show # of VMs that belong to each Resource Set (#7114)
See Zammad#17568
2023-10-23 15:03:30 +02:00
Olivier Lambert
2378399981 docs: update project's README (#7116) 2023-10-23 14:25:03 +02:00
Florent BEAUCHAMP
37b2113763 feat(fs/s3): compute sensible chunk size for uploads 2023-10-23 10:23:50 +02:00
Florent BEAUCHAMP
5048485a85 feat(fs/s3): object lock mode need content md5
and the middleware consume addiitionnal memory
2023-10-23 10:23:50 +02:00
Florent BEAUCHAMP
9e667533e9 fix(fs/s3): throw error if upload >50GB 2023-10-23 10:23:50 +02:00
MlssFrncJrg
1fac7922b4 feat(xo-web/dashboard/health): VDIs to coalesce warning contains the number (#7111)
Fixes Zammad#17577
2023-10-20 15:53:24 +02:00
Julien Fontanet
1a0e5eb6fc chore: format with Prettier 2023-10-20 15:52:10 +02:00
Pierre Donias
321e322492 feat(xo-server/clearHost): pass optional batch size arg (#7107)
Fixes #7105
See https://github.com/xapi-project/xen-api/issues/5202
See https://github.com/xapi-project/xen-api/pull/5203

`host.evacuate`: try passing optional batch size argument.
If not supported: remove it and try again.
2023-10-19 17:03:14 +02:00
Mathieu
8834af65f7 feat(xo-server/xo-web/VM/new): VTPM creation (#7077)
See #7066
See #6802
See #7085
2023-10-19 16:48:56 +02:00
Mathieu
1a1dd0531d feat(xo-web/VM/advanced): VTPM management (#7085)
See #7066
See #6802
See #7074
2023-10-19 15:46:03 +02:00
Pierre Donias
8752487280 docs(installation): add nfs-common dependency for Debian/Ubuntu (#7108) 2023-10-18 22:50:29 +02:00
Pierre Donias
4b12a6d31d fix(xo-server-usage-report): handle null and nested stats (#7092)
Introduced by 083483645e

Fixes Zammad#18120
Fixes Zammad#18266

- Always assume that data can be `null`
- Handle edge cases where all values are `null`
- Properly handle nested RRD collections: collections have different depths (`memory`: 1, `cpus[0]`: 2, `pifs.rx[0]`: 3, ...). This PR replaces `getLastDays` which wouldn't handle those depths properly, with `getDeepLastValues` which is run on the whole stat object and doesn't assume the depth of the collections. It finds any Array at any depth and slices it to only keep the last N values.
2023-10-18 22:50:08 +02:00
Julien Fontanet
2924f82754 fix(xo-web): don't sign out on connection error (#7103)
May fix zammad#17717

Introduced by 005ab47d9
2023-10-18 18:07:16 +02:00
Pierre Donias
9b236a6191 fix(netbox/test): test custom fields first (#7104)
More atomic and it makes more sense for users to check that the Netbox
configuration is correct before doing any write operations
2023-10-18 11:56:10 +02:00
Julien Fontanet
a3b8553cec fix(xo-server,xo-web): fix total number of VDIs to coalesce (#7098)
Fixes #7016

Summing all chains does take not common chains into account, the total must be computed on the server side.
2023-10-18 11:52:43 +02:00
Pierre Donias
00a1778a6d feat(lite): set color-scheme CSS property to "dark" in dark mode (#7101) 2023-10-17 16:50:13 +02:00
MlssFrncJrg
3b6bc629bc fix(xo-web/home): fix misaligned descriptions (#7090) 2023-10-16 15:53:35 +02:00
Pierre Donias
04dfd9a02c fix(xo-server-usage-report): use @xen-orchestra/log to log errors (#7096)
Fixes Zammad#14579
Fixes Zammad#18183

Better handles error objects with a circular structure and avoids "Converting
circular structure to JSON" error on stringify
2023-10-16 10:07:57 +02:00
Pierre Donias
fb52868074 fix(xo-server/patching): always check that XS credentials are configured on XS (#7093)
Introduced by a30d962b1d
2023-10-13 16:49:04 +02:00
Pierre Donias
77d53d2abf fix(xo-server/patching): always pass xsCredentials to installPatches on XS (#7089)
Fixes Zammad#18284

Introduced by a30d962b1d
2023-10-13 11:45:17 +02:00
Julien Fontanet
6afb87def1 feat(xo-server/vm.set): support xenStoreData
Fixes #7055
2023-10-13 11:26:48 +02:00
Mathieu
8bfe293414 feat(lite/VM): add copy, snapshot single action (#7087) 2023-10-12 11:09:11 +02:00
Mathieu
2e634a9d1c feat(xapi/VTPM): ability to create, destroy VTPM (#7074) 2023-10-12 09:19:38 +02:00
Pierre Donias
bea771ca90 fix(xo-server/RPU): do not migrate VM back if already on host (#7071)
See https://xcp-ng.org/forum/topic/7802
2023-10-11 16:16:44 +02:00
Pierre Donias
99e3622f31 feat(xo-web/SelectPif): show network name (#7081)
See Zammad#17381
2023-10-10 15:59:24 +02:00
Pizzosaure
a16522241e docs(netbox): remove extra backtick (#7083)
Introduced by 3b3f927e4b
2023-10-10 14:14:15 +02:00
Julien Fontanet
b86cb12649 chore(yarn.lock): update dev deps 2023-10-09 17:06:54 +02:00
Julien Fontanet
2af74008b2 feat(xo-server-backup-reports): errors are logged as XO tasks 2023-10-09 09:35:24 +02:00
Julien Fontanet
2e689592f1 feat(xo-server-backup-reports): error when transports not enabled 2023-10-09 09:35:24 +02:00
Julien Fontanet
3f8436b58b fix(xo-server/authenticateUser): use clearLogOnSuccess
This fixes success logs not deleted due to race conditions.
2023-10-09 09:35:24 +02:00
Julien Fontanet
e3dd59d684 feat(mixins/Tasks#create): clearLogOnSuccess option 2023-10-09 09:35:24 +02:00
mathieuRA
549d9b70a9 feat(xo-web/host): allow to force smartReboot 2023-10-06 16:52:26 +02:00
mathieuRA
3bf6aae103 feat(xapi/host_smartReboot): ability to bypass blocked operations 2023-10-06 16:52:26 +02:00
Julien Fontanet
afb110c473 fix(fs/rmtree): fix huge memory usage (#7073)
Fixes zammad#15258

This adds a sane concurrency limit of 2 per depth level.

Co-authored-by: Florent BEAUCHAMP <florent.beauchamp@vates.fr>
2023-10-06 09:52:11 +02:00
Pierre Donias
8727c3cf96 docs(patches): update URLs that need to be accessible from XOA (#7075) 2023-10-05 09:45:50 +02:00
Julien Fontanet
b13302ddeb fix(xen-api/cli): dont run default export when imported by ESM
Fix a bug in `@xen-orchestra/xapi` introduced by c3e0308ad

`module.parent` is `null` when the module is the entry point but `undefined` when imported via ESM.
2023-10-04 10:06:17 +02:00
Julien Fontanet
e89ed06314 docs(installation): Node 18 required
XO is not compatible with Node > 18 for the moment, as Node 20 will
likely graduate to LTS soon, the docs must explicitly recommend 18.
2023-10-04 09:25:37 +02:00
Malcolm Scott
e3f57998f7 fix(signin): try to preserve current page across reauthentication (#7013)
If an authentication session expires or is lost for whatever reason, XO redirects to `/signin`.  This redirect generally preserves the URL fragment (hash) which contains the page selected prior to reauthentication, i.e. if the user had been in settings/servers just beforehand, they end up at `/signin#settings/servers`.  However, currently when they log back in they end up on the home page; the page they were on is forgotten.

This commit tries to send the user back to the page they were viewing before reauthentication, by preserving the URL fragment in the login form action / by appending it to the links to authentication plugins.  (Not all authentication plugins will necessarily preserve it internally, but we can optimistically try it and see; at worst the old behaviour will remain.)
2023-10-03 12:39:57 +02:00
Julien Fontanet
8cdb5ee31b chore: update dev deps 2023-10-03 11:24:51 +02:00
Pierre Donias
5b734db656 feat(lite): 0.1.4 (#7068) 2023-10-03 10:26:05 +02:00
rbarhtaoui
e853f9d04f feat(lite): display loading icon and error message when data is not fetched (#6775) 2023-10-03 10:03:44 +02:00
Mathieu
2a5e09719e feat(lite/login): add remember me checkbox (#7030) 2023-10-03 10:01:07 +02:00
Pierre Donias
3c0477e0da feat: release 5.87.0 (#7064) 2023-09-29 11:35:23 +02:00
Pierre Donias
060d1c5297 feat: technical release (#7063) 2023-09-29 10:01:45 +02:00
Julien Fontanet
55dd7bfb9c feat(backups): don't snapshot migrating VMs
Related to zammad#16108
2023-09-28 17:42:43 +02:00
Julien Fontanet
b00cf13029 feat(backups): block snapshot migration during backup
Related to zammad#16108
2023-09-28 17:42:43 +02:00
Julien Fontanet
73755e4ccf feat(xo-server/authenticateUser): log failed attempts
Related to zammad#16318
2023-09-28 17:38:57 +02:00
Julien Fontanet
a1bd96da6a feat(mixins/Tasks#create): allow any properties 2023-09-28 17:38:57 +02:00
mathieuRA
0e934c1413 feat(xo-web/host/advanced): display system disks health 2023-09-28 17:14:09 +02:00
Florent BEAUCHAMP
eb69234a8e feat(xo-server/host): implement smartctl api call 2023-09-28 17:14:09 +02:00
mathieuRA
7659d9c0be fix(xo-web/host/advanced): catch error for ACLs users on hyper threading plugin
it broke the componentDidMount methode and didn't update the state correctly
2023-09-28 17:14:09 +02:00
Florent BEAUCHAMP
2ba81d55f8 fix(vhd-lib/test): collision during tests (#7062)
multiple tests use the same temporary files
2023-09-28 16:49:00 +02:00
Gabriel Gunullu
2e1abad255 feat(xapi/VDI_importContent): add SR name_label to task name_label (#6979) 2023-09-28 16:10:29 +02:00
Julien Fontanet
c7d5b4b063 fix(xo-web/messages): clarify *forget tokens* description
Introduced by c7df11cc6
2023-09-28 15:41:10 +02:00
Julien Fontanet
cc5f4b0996 fix(xo-web/messages): connection token → authentication token
Uniformize naming.
2023-09-28 15:41:06 +02:00
Julien Fontanet
55f627ed83 chore: fix formatting
Introduced by 869f7ffab
2023-09-28 15:37:45 +02:00
Florent BEAUCHAMP
988179a3f0 fix(xo-server): add mbr for cloud-init only for windows VM (#7050)
Fixes zammad#16808
2023-09-28 09:09:13 +02:00
Julien Fontanet
ce617e0732 fix(xo-server/host.restart): make force defaults to false
Introduced by 5ee11c7b6
2023-09-27 17:39:10 +02:00
Florent BEAUCHAMP
f0f429a473 fix(xo-server-backup-report): send report for Mirror Backup (#7049) 2023-09-27 16:39:27 +02:00
Thierry Goettelmann
bb6e158301 feat(lite): host patches (#6709) 2023-09-27 11:44:03 +02:00
Pierre Donias
7ff304a042 feat: technical release (#7058) 2023-09-27 11:30:16 +02:00
Julien Fontanet
7df1994d7f fix(xo-server/sr.getAllUnhealthyVdiChainsLength): require admin permission
Introduced by 0975863d9
2023-09-27 10:37:30 +02:00
Mathieu
a3a2fda157 feat(lite/pool/VMs): ability to snapshot selected VMs (#7021) 2023-09-26 17:28:15 +02:00
Thierry Goettelmann
d8530f9518 chore(lite): update changelog (#7057)
Fixes [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040)
2023-09-26 17:13:29 +02:00
Thierry Goettelmann
d3062ac35c feat(lite/pool/VMs): ability to migrate selected VMs (#7040) 2023-09-26 17:06:00 +02:00
Thierry Goettelmann
b11f11f4db feat(lite): rework modal system (#6994) 2023-09-26 16:25:23 +02:00
Thierry Goettelmann
79d48f3b56 feat(lite/xapi): update XenApi types and enums (#7018) 2023-09-26 15:19:33 +02:00
Pierre Donias
869f7ffab0 feat(xo-web/XOA/Support): button to restart xo-server service (#7056) 2023-09-26 14:35:17 +02:00
Julien Fontanet
6665d6a8e6 chore: format with Prettier 2023-09-26 14:34:47 +02:00
Pierre Donias
8eb0bdbda7 feat(xo-server,xo-web/SR): reclaim space (#7054)
Fixes #1204
2023-09-26 14:21:43 +02:00
Mathieu
710689db0b feat(xo-web/home/host,pool): display product brand and version (#7027) 2023-09-26 11:16:08 +02:00
mathieuRA
801eea7e75 feat(xo-web/host/advanced): confirmation modal for download system logs 2023-09-26 11:10:22 +02:00
Julien Fontanet
7885e1e6e7 feat(xo-web/host/advanced): button do download system logs
Fixes #3968
2023-09-26 11:10:22 +02:00
Julien Fontanet
d384c746ca feat(xo-server/rest-api): export host audit and system logs
See #3968
2023-09-26 11:10:22 +02:00
Pierre Donias
a30d962b1d feat(xo-server,xo-web/patching): support new XS Updates system (#7044)
See Zammad#13416

Support for new XenServer Updates system with authentication:
- User downloads Client ID JSON file from XenServer account
- User uploads it to XO in their user preferences
- XO uses `username` and `apikey` from that file to authenticate and download updates
2023-09-26 10:29:07 +02:00
Pierre Donias
b6e078716b docs(users/auth): update GitHub plugin screenshots (#7035) 2023-09-25 16:10:00 +02:00
Julien Fontanet
34b69c7ee8 chore: refresh yarn.lock
Introduced by 90e0f2684
2023-09-25 09:10:54 +02:00
Julien Fontanet
70bf8d9620 fix(xo-web/kubernetes): handle empty searches domain field
Do not send `['']` if empty.
2023-09-25 09:08:33 +02:00
Florent BEAUCHAMP
c8bfda9cf5 fix(xo-vmdk-to-vhd): handle ova with disk position collision (#7051)
Some OVA have multiple disks with the same position, which prevent the VM from being created (error while creating VBD). Renumeroting the problematic disk works around the issue.

This may lead to unbootable VM in case the renumeroted disk was the bootable one (VMware-VirtualSAN-Witness-7.0.0-15843807.ova for example).

Fixes #7046
2023-09-22 11:44:12 +02:00
Gabriel Gunullu
1eb4c20844 fix(xo-web/kubernetes): remove required property from search domain (#7028)
Make this field optional for the cluster creation.
2023-09-22 09:46:13 +02:00
228 changed files with 7780 additions and 3035 deletions

View File

@@ -13,12 +13,15 @@ describe('decorateWith', () => {
const expectedFn = Function.prototype
const newFn = () => {}
const decorator = decorateWith(function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
const decorator = decorateWith(
function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
return newFn
}, ...expectedArgs)
return newFn
},
...expectedArgs
)
const descriptor = {
configurable: true,

View File

@@ -22,7 +22,7 @@
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.5.0"
"vhd-lib": "^4.6.1"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -7,7 +7,7 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.42.0",
"@xen-orchestra/backups": "^0.43.0",
"@xen-orchestra/fs": "^4.1.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.12",
"version": "1.0.13",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -681,11 +681,13 @@ export class RemoteAdapter {
}
}
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
async outputStream(path, input, { checksum = true, maxStreamLength, streamLength, validator = noop } = {}) {
const container = watchStreamSize(input)
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
maxStreamLength,
streamLength,
async validator() {
await input.task
return validator.apply(this, arguments)

View File

@@ -29,6 +29,8 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
writer =>
writer.run({
stream: forkStreamUnpipe(stream),
// stream will be forked and transformed, it's not safe to attach additionnal properties to it
streamLength: stream.length,
timestamp: metadata.timestamp,
vm: metadata.vm,
vmSnapshot: metadata.vmSnapshot,

View File

@@ -35,13 +35,23 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
useSnapshot: false,
})
)
const vdis = await exportedVm.$getDisks()
let maxStreamLength = 1024 * 1024 // Ovf file and tar headers are a few KB, let's stay safe
for (const vdiRef of vdis) {
const vdi = await this._xapi.getRecord(vdiRef)
// at most the xva will take the physical usage of the disk
// the resulting stream can be smaller due to the smaller block size for xva than vhd, and compression of xcp-ng
maxStreamLength += vdi.physical_utilisation
}
const sizeContainer = watchStreamSize(stream)
const timestamp = Date.now()
await this._callWriters(
writer =>
writer.run({
maxStreamLength,
sizeContainer,
stream: forkStreamUnpipe(stream),
timestamp,

View File

@@ -31,6 +31,11 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
throw new Error('cannot backup a VM created by this very job')
}
const currentOperations = Object.values(vm.current_operations)
if (currentOperations.some(_ => _ === 'migrate_send' || _ === 'pool_migrate')) {
throw new Error('cannot backup a VM currently being migrated')
}
this.config = config
this.job = job
this.remoteAdapters = remoteAdapters
@@ -256,7 +261,15 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
}
if (this._writers.size !== 0) {
await this._copy()
const { pool_migrate = null, migrate_send = null } = this._exportedVm.blocked_operations
const reason = 'VM migration is blocked during backup'
await this._exportedVm.update_blocked_operations({ pool_migrate: reason, migrate_send: reason })
try {
await this._copy()
} finally {
await this._exportedVm.update_blocked_operations({ pool_migrate, migrate_send })
}
}
} finally {
if (startAfter) {

View File

@@ -24,7 +24,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
)
}
async _run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
async _run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
const settings = this._settings
const job = this._job
const scheduleId = this._scheduleId
@@ -65,6 +65,8 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
await Task.run({ name: 'transfer' }, async () => {
await adapter.outputStream(dataFilename, stream, {
maxStreamLength,
streamLength,
validator: tmpPath => adapter.isValidXva(tmpPath),
})
return { size: sizeContainer.size }

View File

@@ -1,9 +1,9 @@
import { AbstractWriter } from './_AbstractWriter.mjs'
export class AbstractFullWriter extends AbstractWriter {
async run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
async run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
try {
return await this._run({ timestamp, sizeContainer, stream, vm, vmSnapshot })
return await this._run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot })
} finally {
// ensure stream is properly closed
stream.destroy()

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.42.0",
"version": "0.43.0",
"engines": {
"node": ">=14.18"
},
@@ -44,7 +44,7 @@
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"vhd-lib": "^4.6.1",
"xen-api": "^1.3.6",
"yazl": "^2.5.1"
},
@@ -56,7 +56,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^3.1.0"
"@xen-orchestra/xapi": "^3.2.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -189,7 +189,7 @@ export default class RemoteHandlerAbstract {
* @param {number} [options.dirMode]
* @param {(this: RemoteHandlerAbstract, path: string) => Promise<undefined>} [options.validator] Function that will be called before the data is commited to the remote, if it fails, file should not exist
*/
async outputStream(path, input, { checksum = true, dirMode, validator } = {}) {
async outputStream(path, input, { checksum = true, dirMode, maxStreamLength, streamLength, validator } = {}) {
path = normalizePath(path)
let checksumStream
@@ -201,6 +201,8 @@ export default class RemoteHandlerAbstract {
}
await this._outputStream(path, input, {
dirMode,
maxStreamLength,
streamLength,
validator,
})
if (checksum) {
@@ -624,14 +626,18 @@ export default class RemoteHandlerAbstract {
const files = await this._list(dir)
await asyncEach(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
})
this._unlink(`${dir}/${file}`).catch(
error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
},
// real unlink concurrency will be 2**max directory depth
{ concurrency: 2 }
)
)
return this._rmtree(dir)
}

View File

@@ -5,6 +5,7 @@ import {
CreateMultipartUploadCommand,
DeleteObjectCommand,
GetObjectCommand,
GetObjectLockConfigurationCommand,
HeadObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
@@ -17,7 +18,7 @@ import { getApplyMd5BodyChecksumPlugin } from '@aws-sdk/middleware-apply-body-ch
import { Agent as HttpAgent } from 'http'
import { Agent as HttpsAgent } from 'https'
import { createLogger } from '@xen-orchestra/log'
import { PassThrough, pipeline } from 'stream'
import { PassThrough, Transform, pipeline } from 'stream'
import { parse } from 'xo-remote-parser'
import copyStreamToBuffer from './_copyStreamToBuffer.js'
import guessAwsRegion from './_guessAwsRegion.js'
@@ -30,6 +31,8 @@ import { pRetry } from 'promise-toolbox'
// limits: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
const MAX_PART_NUMBER = 10000
const MIN_PART_SIZE = 5 * 1024 * 1024
const { warn } = createLogger('xo:fs:s3')
export default class S3Handler extends RemoteHandlerAbstract {
@@ -71,9 +74,6 @@ export default class S3Handler extends RemoteHandlerAbstract {
}),
})
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
const parts = split(path)
this.#bucket = parts.shift()
this.#dir = join(...parts)
@@ -223,11 +223,35 @@ export default class S3Handler extends RemoteHandlerAbstract {
}
}
async _outputStream(path, input, { validator }) {
async _outputStream(path, input, { streamLength, maxStreamLength = streamLength, validator }) {
// S3 storage is limited to 10K part, each part is limited to 5GB. And the total upload must be smaller than 5TB
// a bigger partSize increase the memory consumption of aws/lib-storage exponentially
let partSize
if (maxStreamLength === undefined) {
warn(`Writing ${path} to a S3 remote without a max size set will cut it to 50GB`, { path })
partSize = MIN_PART_SIZE // min size for S3
} else {
partSize = Math.min(Math.max(Math.ceil(maxStreamLength / MAX_PART_NUMBER), MIN_PART_SIZE), MAX_PART_SIZE)
}
// ensure we don't try to upload a stream to big for this partSize
let readCounter = 0
const MAX_SIZE = MAX_PART_NUMBER * partSize
const streamCutter = new Transform({
transform(chunk, encoding, callback) {
readCounter += chunk.length
if (readCounter > MAX_SIZE) {
callback(new Error(`read ${readCounter} bytes, maximum size allowed is ${MAX_SIZE} `))
} else {
callback(null, chunk)
}
},
})
// Workaround for "ReferenceError: ReadableStream is not defined"
// https://github.com/aws/aws-sdk-js-v3/issues/2522
const Body = new PassThrough()
pipeline(input, Body, () => {})
pipeline(input, streamCutter, Body, () => {})
const upload = new Upload({
client: this.#s3,
@@ -235,6 +259,8 @@ export default class S3Handler extends RemoteHandlerAbstract {
...this.#createParams(path),
Body,
},
partSize,
leavePartsOnError: false,
})
await upload.done()
@@ -418,6 +444,24 @@ export default class S3Handler extends RemoteHandlerAbstract {
async _closeFile(fd) {}
async _sync() {
await super._sync()
try {
// if Object Lock is enabled, each upload must come with a contentMD5 header
// the computation of this md5 is memory-intensive, especially when uploading a stream
const res = await this.#s3.send(new GetObjectLockConfigurationCommand({ Bucket: this.#bucket }))
if (res.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled') {
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
// will automatically add the contentMD5 header to any upload to S3
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
}
} catch (error) {
if (error.Code !== 'ObjectLockConfigurationNotFoundError') {
throw error
}
}
}
useVhdDirectory() {
return true
}

View File

@@ -2,6 +2,17 @@
## **next**
- Ability to snapshot/copy a VM from its view (PR [#7087](https://github.com/vatesfr/xen-orchestra/pull/7087))
- [Header] Replace logo with "XO LITE" (PR [#7118](https://github.com/vatesfr/xen-orchestra/pull/7118))
- New VM console toolbar + Ability to send Ctrl+Alt+Del (PR [#7088](https://github.com/vatesfr/xen-orchestra/pull/7088))
## **0.1.4** (2023-10-03)
- Ability to migrate selected VMs to another host (PR [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040))
- Ability to snapshot selected VMs (PR [#7021](https://github.com/vatesfr/xen-orchestra/pull/7021))
- Add Patches to Pool Dashboard (PR [#6709](https://github.com/vatesfr/xen-orchestra/pull/6709))
- Add remember me checkbox on the login page (PR [#7030](https://github.com/vatesfr/xen-orchestra/pull/7030))
## **0.1.3** (2023-09-01)
- Add Alarms to Pool Dashboard (PR [#6976](https://github.com/vatesfr/xen-orchestra/pull/6976))

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.3",
"version": "0.1.4",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",
@@ -11,6 +11,7 @@
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@fontsource/poppins": "^5.0.8",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",

View File

@@ -1,7 +1,11 @@
@import "reset.css";
@import "theme.css";
/* TODO Serve fonts locally */
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;0,900;1,400;1,500;1,600;1,700;1,900&display=swap");
@import "@fontsource/poppins/400.css";
@import "@fontsource/poppins/500.css";
@import "@fontsource/poppins/600.css";
@import "@fontsource/poppins/700.css";
@import "@fontsource/poppins/900.css";
@import "@fontsource/poppins/400-italic.css";
body {
min-height: 100vh;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,4 +1,6 @@
:root {
--color-logo: #282467;
--color-blue-scale-000: #000000;
--color-blue-scale-100: #1a1b38;
--color-blue-scale-200: #595a6f;
@@ -59,6 +61,10 @@
}
:root.dark {
color-scheme: dark;
--color-logo: #e5e5e7;
--color-blue-scale-000: #ffffff;
--color-blue-scale-100: #e5e5e7;
--color-blue-scale-200: #9899a5;

View File

@@ -7,7 +7,8 @@
class="toggle-navigation"
/>
<RouterLink :to="{ name: 'home' }">
<img alt="XO Lite" src="../assets/logo.svg" />
<img v-if="isMobile" alt="XO Lite" src="../assets/logo.svg" />
<TextLogo v-else />
</RouterLink>
<slot />
<div class="right">
@@ -18,6 +19,7 @@
<script lang="ts" setup>
import AccountButton from "@/components/AccountButton.vue";
import TextLogo from "@/components/TextLogo.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useUiStore } from "@/stores/ui.store";
@@ -44,6 +46,10 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
img {
width: 4rem;
}
.text-logo {
margin: 1rem;
}
}
.right {

View File

@@ -16,6 +16,10 @@
required
/>
</FormInputWrapper>
<label class="remember-me-label">
<FormCheckbox v-model="rememberMe" />
<p>{{ $t("keep-me-logged") }}</p>
</label>
<UiButton type="submit" :busy="isConnecting">
{{ $t("login") }}
</UiButton>
@@ -28,6 +32,9 @@ import { usePageTitleStore } from "@/stores/page-title.store";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useLocalStorage } from "@vueuse/core";
import FormCheckbox from "@/components/form/FormCheckbox.vue";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import UiButton from "@/components/ui/UiButton.vue";
@@ -42,12 +49,16 @@ const password = ref("");
const error = ref<string>();
const passwordRef = ref<InstanceType<typeof FormInput>>();
const isInvalidPassword = ref(false);
const rememberMe = useLocalStorage("rememberMe", false);
const focusPasswordInput = () => passwordRef.value?.focus();
onMounted(() => {
xenApiStore.reconnect();
focusPasswordInput();
if (rememberMe.value) {
xenApiStore.reconnect();
} else {
focusPasswordInput();
}
});
watch(password, () => {
@@ -72,6 +83,19 @@ async function handleSubmit() {
</script>
<style lang="postcss" scoped>
.remember-me-label {
cursor: pointer;
width: fit-content;
& .form-checkbox {
margin: 1rem 1rem 1rem 0;
vertical-align: middle;
}
& p {
display: inline;
vertical-align: middle;
}
}
.form-container {
display: flex;
align-items: center;
@@ -87,7 +111,6 @@ form {
font-size: 2rem;
min-width: 30em;
max-width: 100%;
align-items: center;
flex-direction: column;
justify-content: center;
margin: 0 auto;
@@ -104,7 +127,7 @@ h1 {
img {
width: 40rem;
margin-bottom: 5rem;
margin: auto auto 5rem auto;
}
input {
@@ -118,6 +141,6 @@ input {
}
button {
margin-top: 2rem;
margin: 2rem auto;
}
</style>

View File

@@ -14,66 +14,66 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faFilter"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ property }}
</UiBadge>
</div>
</div>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
>
{{ property }}
</UiBadge>
</div>
</div>
</template>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
import type { Filters, NewFilter } from "@/types/filter";
import { faFilter, faPlus } from "@fortawesome/free-solid-svg-icons";
import CollectionFilterRow from "@/components/CollectionFilterRow.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiBadge from "@/components/ui/UiBadge.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { getFilterIcon } from "@/libs/utils";
import type { Filters, NewFilter } from "@/types/filter";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
defineProps<{
activeFilters: string[];
@@ -85,7 +85,7 @@ const emit = defineEmits<{
(event: "removeFilter", filter: string): void;
}>();
const { isOpen, open, close } = useModal();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const newFilters = ref<NewFilter[]>([]);
let newFilterId = 0;
@@ -156,11 +156,6 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>
@@ -190,4 +185,10 @@ const handleCancel = () => {
margin-top: 0.6rem;
gap: 0.5rem;
}
.rows {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -219,7 +219,6 @@ const valueInputAfter = computed(() =>
.collection-filter-row {
display: flex;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid var(--background-color-secondary);
gap: 1rem;
@@ -242,4 +241,8 @@ const valueInputAfter = computed(() =>
.form-widget-advanced {
flex: 1;
}
.ui-action-button:first-of-type {
margin-left: auto;
}
</style>

View File

@@ -17,56 +17,56 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faSort"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
</template>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import FormWidget from "@/components/FormWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import type { ActiveSorts, Sorts } from "@/types/sort";
import {
faCaretDown,
faCaretUp,
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { ref } from "vue";
@@ -81,7 +81,7 @@ const emit = defineEmits<{
(event: "removeSort", property: string): void;
}>();
const { isOpen, open, close } = useModal();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const newSortProperty = ref();
const newSortIsAscending = ref<boolean>(true);
@@ -96,11 +96,6 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>

View File

@@ -28,13 +28,9 @@
</tr>
</thead>
<tbody>
<tr v-for="item in filteredAndSortedCollection" :key="item[idProperty]">
<tr v-for="item in filteredAndSortedCollection" :key="item.$ref">
<td v-if="isSelectable">
<input
v-model="selected"
:value="item[props.idProperty]"
type="checkbox"
/>
<input v-model="selected" :value="item.$ref" type="checkbox" />
</td>
<slot :item="item" name="body-row" />
</tr>
@@ -42,10 +38,7 @@
</UiTable>
</template>
<script lang="ts" setup>
import { computed, toRef, watch } from "vue";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
<script generic="T extends XenApiRecord<any>" lang="ts" setup>
import CollectionFilter from "@/components/CollectionFilter.vue";
import CollectionSorter from "@/components/CollectionSorter.vue";
import UiTable from "@/components/ui/UiTable.vue";
@@ -54,17 +47,20 @@ import useCollectionSorter from "@/composables/collection-sorter.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useMultiSelect from "@/composables/multi-select.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiRecord } from "@/libs/xen-api/xen-api.types";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
import { computed, toRef, watch } from "vue";
const props = defineProps<{
modelValue?: string[];
modelValue?: T["$ref"][];
availableFilters?: Filters;
availableSorts?: Sorts;
collection: Record<string, any>[];
idProperty: string;
collection: T[];
}>();
const emit = defineEmits<{
(event: "update:modelValue", selectedRefs: string[]): void;
(event: "update:modelValue", selectedRefs: T["$ref"][]): void;
}>();
const isSelectable = computed(() => props.modelValue !== undefined);
@@ -85,12 +81,10 @@ const filteredAndSortedCollection = useSortedCollection(
compareFn
);
const usableRefs = computed(() =>
props.collection.map((item) => item[props.idProperty])
);
const usableRefs = computed(() => props.collection.map((item) => item["$ref"]));
const selectableRefs = computed(() =>
filteredAndSortedCollection.value.map((item) => item[props.idProperty])
filteredAndSortedCollection.value.map((item) => item["$ref"])
);
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs);

View File

@@ -0,0 +1,71 @@
<template>
<UiCardSpinner v-if="!areSomeLoaded" />
<UiTable v-else class="hosts-patches-table" :class="{ desktop: isDesktop }">
<tr v-for="patch in sortedPatches" :key="patch.$id">
<th>{{ patch.name }}</th>
<td>
<div class="version">
{{ patch.version }}
<template v-if="hasMultipleHosts">
<UiSpinner v-if="!areAllLoaded" />
<UiCounter
v-else
v-tooltip="{
placement: 'left',
content: $t('n-hosts-awaiting-patch', {
n: patch.$hostRefs.size,
}),
}"
:value="patch.$hostRefs.size"
class="counter"
color="error"
/>
</template>
</div>
</td>
</tr>
</UiTable>
</template>
<script lang="ts" setup>
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTable from "@/components/ui/UiTable.vue";
import type { XenApiPatchWithHostRefs } from "@/composables/host-patches.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import { useUiStore } from "@/stores/ui.store";
import { computed } from "vue";
const props = defineProps<{
patches: XenApiPatchWithHostRefs[];
hasMultipleHosts: boolean;
areAllLoaded: boolean;
areSomeLoaded: boolean;
}>();
const sortedPatches = computed(() =>
[...props.patches].sort(
(patch1, patch2) => patch1.changelog.date - patch2.changelog.date
)
);
const { isDesktop } = useUiStore();
</script>
<style lang="postcss" scoped>
.hosts-patches-table.desktop {
max-width: 45rem;
}
.version {
display: flex;
gap: 1rem;
justify-content: flex-end;
align-items: center;
}
.counter {
font-size: 1rem;
}
</style>

View File

@@ -66,8 +66,8 @@ onUnmounted(() => {
store.value?.unsubscribe(subscriptionId);
});
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(() =>
store.value?.getByUuid(props.uuid as any)
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(
() => store.value?.getByUuid(props.uuid as any)
);
const isReady = computed(() => {

View File

@@ -4,7 +4,7 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import {
faMoon,
faPause,
@@ -15,14 +15,14 @@ import {
import { computed } from "vue";
const props = defineProps<{
state: POWER_STATE;
state: VM_POWER_STATE;
}>();
const icons = {
[POWER_STATE.RUNNING]: faPlay,
[POWER_STATE.PAUSED]: faPause,
[POWER_STATE.SUSPENDED]: faMoon,
[POWER_STATE.HALTED]: faStop,
[VM_POWER_STATE.RUNNING]: faPlay,
[VM_POWER_STATE.PAUSED]: faPause,
[VM_POWER_STATE.SUSPENDED]: faMoon,
[VM_POWER_STATE.HALTED]: faStop,
};
const icon = computed(() => icons[props.state] ?? faQuestion);

View File

@@ -105,6 +105,10 @@ watchEffect(() => {
onBeforeUnmount(() => {
clearVncClient();
});
defineExpose({
sendCtrlAltDel: () => vncClient?.sendCtrlAltDel(),
});
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,37 @@
<template>
<svg
class="text-logo"
viewBox="300.85 622.73 318.32 63.27"
xmlns="http://www.w3.org/2000/svg"
width="100"
height="22"
>
<g>
<polygon
points="355.94 684.92 341.54 684.92 327.84 664.14 315.68 684.92 301.81 684.92 317.59 659.25 338.96 659.25 355.94 684.92"
/>
<path
d="M406.2,627.17c4.62,2.64,8.27,6.33,10.94,11.07,2.67,4.74,4.01,10.1,4.01,16.07s-1.34,11.35-4.01,16.12c-2.67,4.77-6.32,8.48-10.94,11.12-4.63,2.64-9.78,3.97-15.47,3.97s-10.85-1.32-15.47-3.97c-4.63-2.64-8.27-6.35-10.95-11.12-2.67-4.77-4.01-10.14-4.01-16.12s1.34-11.33,4.01-16.07c2.67-4.74,6.32-8.43,10.95-11.07,4.62-2.64,9.78-3.97,15.47-3.97s10.84,1.32,15.47,3.97Zm-24.86,9.65c-2.7,1.61-4.81,3.92-6.33,6.94-1.52,3.02-2.28,6.54-2.28,10.56s.76,7.54,2.28,10.56c1.52,3.02,3.63,5.33,6.33,6.94,2.7,1.61,5.83,2.41,9.39,2.41s6.69-.8,9.39-2.41c2.7-1.61,4.81-3.92,6.33-6.94,1.52-3.02,2.28-6.53,2.28-10.56s-.76-7.54-2.28-10.56-3.63-5.33-6.33-6.94c-2.7-1.61-5.83-2.41-9.39-2.41s-6.69,.8-9.39,2.41Z"
/>
<polygon
points="354.99 624.06 339.53 649.22 317.49 649.22 300.86 624.06 315.26 624.06 328.96 644.84 341.12 624.06 354.99 624.06"
/>
<g>
<path d="M476.32,675.94h20.81v10.04h-33.47v-63.14h12.66v53.1Z" />
<path d="M517.84,622.84v63.14h-12.66v-63.14h12.66Z" />
<path
d="M573.29,622.84v10.22h-16.82v52.92h-12.66v-52.92h-16.83v-10.22h46.31Z"
/>
<path
d="M595.18,633.06v15.83h21.26v10.04h-21.26v16.73h23.97v10.31h-36.64v-63.23h36.64v10.31h-23.97Z"
/>
</g>
</g>
</svg>
</template>
<style lang="postcss" scoped>
.text-logo {
fill: var(--color-logo);
}
</style>

View File

@@ -1,45 +1,58 @@
<template>
<UiModal
v-if="isSslModalOpen"
:icon="faServer"
color="error"
@close="clearUnreachableHostsUrls"
>
<template #title>{{ $t("unreachable-hosts") }}</template>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
</template>
<UiModal v-model="isSslModalOpen" color="error">
<ConfirmModalLayout :icon="faServer">
<template #title>{{ $t("unreachable-hosts") }}</template>
<template #default>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
</template>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="closeSslModal">{{ $t("cancel") }}</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import useModal from "@/composables/modal.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { faServer } from "@fortawesome/free-solid-svg-icons";
import UiModal from "@/components/ui/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { computed, ref, watch } from "vue";
import { difference } from "lodash-es";
import { ref, watch } from "vue";
const { records: hosts } = useHostCollection();
const unreachableHostsUrls = ref<Set<string>>(new Set());
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);
const reload = () => window.location.reload();
const { isOpen: isSslModalOpen, close: closeSslModal } = useModal({
onClose: () => unreachableHostsUrls.value.clear(),
});
watch(
() => unreachableHostsUrls.value.size,
(size) => {
isSslModalOpen.value = size > 0;
},
{ immediate: true }
);
watch(hosts, (nextHosts, previousHosts) => {
difference(nextHosts, previousHosts).forEach((host) => {
const url = new URL("http://localhost");
@@ -53,7 +66,11 @@ watch(hosts, (nextHosts, previousHosts) => {
</script>
<style lang="postcss" scoped>
.description p {
margin: 1rem 0;
.description {
text-align: center;
p {
margin: 1rem 0;
}
}
</style>

View File

@@ -2,12 +2,7 @@
```vue
<template>
<LinearChart
title="Chart title"
subtitle="Chart subtitle"
:data="data"
:value-formatter="customValueFormatter"
/>
<LinearChart :data="data" :value-formatter="customValueFormatter" />
</template>
<script lang="ts" setup>

View File

@@ -1,12 +1,8 @@
<template>
<UiCard class="linear-chart">
<VueCharts :option="option" autoresize class="chart" />
<slot name="summary" />
</UiCard>
<VueCharts :option="option" autoresize class="chart" />
</template>
<script lang="ts" setup>
import UiCard from "@/components/ui/UiCard.vue";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_CHART_VALUE_FORMATTER } from "@/types/injection-keys";
import { utcFormat } from "d3-time-format";
@@ -15,7 +11,6 @@ import { LineChart } from "echarts/charts";
import {
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from "echarts/components";
import { use } from "echarts/core";
@@ -26,8 +21,6 @@ import VueCharts from "vue-echarts";
const Y_AXIS_MAX_VALUE = 200;
const props = defineProps<{
title?: string;
subtitle?: string;
data: LinearChartData;
valueFormatter?: ValueFormatter;
maxValue?: number;
@@ -52,15 +45,10 @@ use([
LineChart,
GridComponent,
TooltipComponent,
TitleComponent,
LegendComponent,
]);
const option = computed<EChartsOption>(() => ({
title: {
text: props.title,
subtext: props.subtitle,
},
legend: {
data: props.data.map((series) => series.label),
},

View File

@@ -58,7 +58,7 @@ const getDefaultOpenedDirectories = (): Set<string> => {
}
const openedDirectories = new Set<string>();
const parts = currentRoute.path.split("/");
const parts = currentRoute.path.split("/").slice(2);
let currentPath = "";
for (const part of parts) {

View File

@@ -1,6 +1,8 @@
<template>
<UiModal v-if="isRawValueModalOpen" @close="closeRawValueModal">
<CodeHighlight :code="rawValueModalPayload" />
<UiModal v-model="isRawValueModalOpen">
<BasicModalLayout>
<CodeHighlight :code="rawValueModalPayload" />
</BasicModalLayout>
</UiModal>
<StoryParamsTable>
<thead>
@@ -99,7 +101,8 @@ import CodeHighlight from "@/components/CodeHighlight.vue";
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
import StoryWidget from "@/components/component-story/StoryWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import useModal from "@/composables/modal.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
@@ -130,7 +133,6 @@ const model = useVModel(props, "modelValue", emit);
const {
open: openRawValueModal,
close: closeRawValueModal,
isOpen: isRawValueModalOpen,
payload: rawValueModalPayload,
} = useModal<string>();

View File

@@ -4,7 +4,7 @@
v-if="label !== undefined || learnMoreUrl !== undefined"
class="label-container"
>
<label :for="id" class="label">
<label :class="{ light }" :for="id" class="label">
<UiIcon :icon="icon" />
{{ label }}
</label>
@@ -58,6 +58,7 @@ const props = withDefaults(
error?: string;
help?: string;
disabled?: boolean;
light?: boolean;
}>(),
{ disabled: undefined }
);
@@ -95,14 +96,24 @@ useContext(DisabledContext, () => props.disabled);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.label {
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
font-size: 1.4rem;
padding: 1rem 0;
&.light {
font-size: 1.6rem;
color: var(--color-blue-scale-300);
font-weight: 400;
}
&:not(.light) {
font-size: 1.4rem;
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
}
}
.messages-container {

View File

@@ -1,20 +1,28 @@
<template>
<UiModal
@submit.prevent="saveJson"
v-model="isCodeModalOpen"
:color="isJsonValid ? 'success' : 'error'"
v-if="isCodeModalOpen"
:icon="faCode"
@close="closeCodeModal"
closable
>
<FormTextarea class="modal-textarea" v-model="editedJson" />
<template #buttons>
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
<UiButton :disabled="!isJsonValid" type="submit"
>{{ $t("save") }}
</UiButton>
</template>
<FormModalLayout @submit.prevent="saveJson" :icon="faCode">
<template #default>
<FormTextarea class="modal-textarea" v-model="editedJson" />
</template>
<template #buttons>
<UiButton transparent @click="formatJson">
{{ $t("reformat") }}
</UiButton>
<UiButton outlined @click="closeCodeModal">
{{ $t("cancel") }}
</UiButton>
<UiButton :disabled="!isJsonValid" type="submit">
{{ $t("save") }}
</UiButton>
</template>
</FormModalLayout>
</UiModal>
<FormInput
@click="openCodeModal"
:model-value="jsonValue"
@@ -26,8 +34,9 @@
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import FormTextarea from "@/components/form/FormTextarea.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { useVModel, whenever } from "@vueuse/core";

View File

@@ -25,10 +25,11 @@ defineProps<{
align-items: center;
height: 4.4rem;
padding-right: 1.5rem;
padding-left: 1rem;
padding-left: 1.5rem;
white-space: nowrap;
border-radius: 0.8rem;
gap: 1rem;
background-color: var(--color-blue-scale-500);
&.disabled {
color: var(--color-blue-scale-400);

View File

@@ -41,11 +41,11 @@ import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
import { percent } from "@/libs/utils";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
const ACTIVE_STATES = new Set([POWER_STATE.RUNNING, POWER_STATE.PAUSED]);
const ACTIVE_STATES = new Set([VM_POWER_STATE.RUNNING, VM_POWER_STATE.PAUSED]);
const {
hasError: hostStoreHasError,

View File

@@ -0,0 +1,41 @@
<template>
<UiCard>
<UiCardTitle class="patches-title">
{{ $t("patches") }}
<template v-if="areAllLoaded" #right>
{{ $t("n-missing", { n: count }) }}
</template>
</UiCardTitle>
<div class="table-container">
<HostPatches
:are-all-loaded="areAllLoaded"
:are-some-loaded="areSomeLoaded"
:has-multiple-hosts="hosts.length > 1"
:patches="patches"
/>
</div>
</UiCard>
</template>
<script lang="ts" setup>
import HostPatches from "@/components/HostPatchesTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostPatches } from "@/composables/host-patches.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { records: hosts } = useHostCollection();
const { count, patches, areSomeLoaded, areAllLoaded } = useHostPatches(hosts);
</script>
<style lang="postcss" scoped>
.patches-title {
--section-title-right-color: var(--color-red-vates-base);
}
.table-container {
max-height: 40rem;
overflow: auto;
}
</style>

View File

@@ -1,33 +1,44 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoData component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('network-throughput')"
:value-formatter="customValueFormatter"
/>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("network-throughput") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
</UiCard>
</template>
<script lang="ts" setup>
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { computed, defineAsyncComponent, inject } from "vue";
import { map } from "lodash-es";
import { useI18n } from "vue-i18n";
import { formatSize } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData } from "@/types/chart";
import { map } from "lodash-es";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { hasError, isFetching } = useHostCollection();
const data = computed<LinearChartData>(() => {
const stats = hostLastWeekStats?.stats?.value;
@@ -82,6 +93,25 @@ const data = computed<LinearChartData>(() => {
];
});
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null &&
Object.values(hostStats.pifs["rx"])[0].length +
Object.values(hostStats.pifs["tx"])[0].length ===
data.value[0].data.length + data.value[1].data.length
);
});
});
const isLoading = computed(() => isFetching.value || !isStatFetched.value);
// TODO: improve the way to get the max value of graph
// See: https://github.com/vatesfr/xen-orchestra/pull/6610/files#r1072237279
const customMaxValue = computed(

View File

@@ -1,22 +1,23 @@
<template>
<UiCardTitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
subtitle
/>
<NoDataError v-if="hasError" />
<UsageBar v-else :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { computed, inject, type ComputedRef } from "vue";
import { getAvgCpuUsage } from "@/libs/utils";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { hasError } = useHostCollection();

View File

@@ -1,24 +1,33 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: Display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-cpu-usage')"
:value-formatter="customValueFormatter"
/>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("pool-cpu-usage") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
</UiCard>
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import type { HostStats } from "@/libs/xapi-stats";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
import { computed, defineAsyncComponent, inject } from "vue";
import type { HostStats } from "@/libs/xapi-stats";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { sumBy } from "lodash-es";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useI18n } from "vue-i18n";
const LinearChart = defineAsyncComponent(
@@ -29,8 +38,7 @@ const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { records: hosts } = useHostCollection();
const { records: hosts, isFetching, hasError } = useHostCollection();
const customMaxValue = computed(
() => 100 * sumBy(hosts.value, (host) => +host.cpu_info.cpu_count)
);
@@ -79,6 +87,22 @@ const data = computed<LinearChartData>(() => {
},
];
});
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null &&
Object.values(hostStats.cpus)[0].length === data.value[0].data.length
);
});
});
const isLoading = computed(() => isFetching.value || !isStatFetched.value);
const customValueFormatter: ValueFormatter = (value) => `${value}%`;
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -9,6 +9,7 @@
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
@@ -16,7 +17,7 @@ import { useVmCollection } from "@/stores/xen-api/vm.store";
import { getAvgCpuUsage } from "@/libs/utils";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import { UiCardTitleLevel } from "@/types/enums";
const { hasError } = useVmCollection();

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -13,6 +13,7 @@ import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { type ComputedRef, computed, inject } from "vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";

View File

@@ -1,37 +1,43 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-ram-usage')"
:value-formatter="customValueFormatter"
>
<template #summary>
<SizeStatsSummary :size="currentData.size" :usage="currentData.usage" />
</template>
</LinearChart>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("pool-ram-usage") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
<SizeStatsSummary :size="currentData.size" :usage="currentData.usage" />
</UiCard>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, inject } from "vue";
import { formatSize } from "@/libs/utils";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData } from "@/types/chart";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import { sumBy } from "lodash-es";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { formatSize } from "@/libs/utils";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
import { computed, defineAsyncComponent, inject } from "vue";
import { useI18n } from "vue-i18n";
const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { runningHosts } = useHostCollection();
const { runningHosts, isFetching, hasError } = useHostCollection();
const { getHostMemory } = useHostMetricsCollection();
const { t } = useI18n();
@@ -92,6 +98,23 @@ const data = computed<LinearChartData>(() => {
];
});
const customValueFormatter: ValueFormatter = (value) =>
String(formatSize(value));
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null && hostStats.memory.length === data.value[0].data.length
);
});
});
const isLoading = computed(
() => (isFetching.value && !hasError.value) || !isStatFetched.value
);
const customValueFormatter = (value: number) => String(formatSize(value));
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -9,14 +9,15 @@
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { computed, inject, type ComputedRef } from "vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
const { hasError } = useVmCollection();

View File

@@ -1,35 +1,40 @@
<template>
<div :class="{ subtitle }" class="ui-section-title">
<component
:is="subtitle ? 'h5' : 'h4'"
v-if="$slots.default || left"
class="left"
>
<div :class="['ui-section-title', tags.left]">
<component :is="tags.left" v-if="$slots.default || left" class="left">
<slot>{{ left }}</slot>
<UiCounter class="count" v-if="count > 0" :value="count" color="info" />
</component>
<component
:is="subtitle ? 'h6' : 'h5'"
v-if="$slots.right || right"
class="right"
>
<component :is="tags.right" v-if="$slots.right || right" class="right">
<slot name="right">{{ right }}</slot>
</component>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import { UiCardTitleLevel } from "@/types/enums";
withDefaults(
const props = withDefaults(
defineProps<{
subtitle?: boolean;
count?: number;
level?: UiCardTitleLevel;
left?: string;
right?: string;
count?: number;
}>(),
{ count: 0 }
{ count: 0, level: UiCardTitleLevel.Title }
);
const tags = computed(() => {
switch (props.level) {
case UiCardTitleLevel.Subtitle:
return { left: "h6", right: "h6" };
case UiCardTitleLevel.SubtitleWithUnderline:
return { left: "h5", right: "h6" };
default:
return { left: "h4", right: "h5" };
}
});
</script>
<style lang="postcss" scoped>
@@ -37,7 +42,6 @@ withDefaults(
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
--section-title-left-size: 2rem;
--section-title-left-color: var(--color-blue-scale-100);
@@ -46,9 +50,17 @@ withDefaults(
--section-title-right-color: var(--color-extra-blue-base);
--section-title-right-weight: 700;
&.subtitle {
border-bottom: 1px solid var(--color-extra-blue-base);
&.h6 {
margin-bottom: 1rem;
--section-title-left-size: 1.5rem;
--section-title-left-color: var(--color-blue-scale-300);
--section-title-left-weight: 400;
}
&.h5 {
margin-top: 2rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-extra-blue-base);
--section-title-left-size: 1.6rem;
--section-title-left-color: var(--color-extra-blue-base);
--section-title-left-weight: 700;

View File

@@ -1,157 +0,0 @@
<template>
<Teleport to="body">
<form
:class="className"
class="ui-modal"
v-bind="$attrs"
@click.self="emit('close')"
>
<div class="container">
<span v-if="onClose" class="close-icon" @click="emit('close')">
<UiIcon :icon="faXmark" />
</span>
<div v-if="icon || $slots.icon" class="modal-icon">
<slot name="icon">
<UiIcon :icon="icon" />
</slot>
</div>
<UiTitle v-if="$slots.title" type="h4">
<slot name="title" />
</UiTitle>
<div v-if="$slots.subtitle" class="subtitle">
<slot name="subtitle" />
</div>
<div v-if="$slots.default" class="content">
<slot />
</div>
<UiButtonGroup :color="color">
<slot name="buttons" />
</UiButtonGroup>
</div>
</form>
</Teleport>
</template>
<script lang="ts" setup>
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useMagicKeys, whenever } from "@vueuse/core";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
color?: "info" | "warning" | "error" | "success";
onClose?: () => void;
}>(),
{ color: "info" }
);
const emit = defineEmits<{
(event: "close"): void;
}>();
const { escape } = useMagicKeys();
whenever(escape, () => emit("close"));
const className = computed(() => {
return [`color-${props.color}`, { "has-icon": props.icon !== undefined }];
});
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background-color: #00000080;
}
.color-success {
--modal-color: var(--color-green-infra-base);
--modal-background-color: var(--background-color-green-infra);
}
.color-info {
--modal-color: var(--color-extra-blue-base);
--modal-background-color: var(--background-color-extra-blue);
}
.color-warning {
--modal-color: var(--color-orange-world-base);
--modal-background-color: var(--background-color-orange-world);
}
.color-error {
--modal-color: var(--color-red-vates-base);
--modal-background-color: var(--background-color-red-vates);
}
.container {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
min-width: 40rem;
padding: 4.2rem;
text-align: center;
border-radius: 1rem;
background-color: var(--modal-background-color);
box-shadow: var(--shadow-400);
}
.close-icon {
font-size: 2rem;
position: absolute;
top: 1.5rem;
right: 2rem;
padding: 0.2rem 0.5rem;
cursor: pointer;
color: var(--modal-color);
}
.container :deep(.accent) {
color: var(--modal-color);
}
.modal-icon {
font-size: 4.8rem;
margin: 2rem 0;
color: var(--modal-color);
}
.ui-title {
margin-top: 4rem;
.has-icon & {
margin-top: 0;
}
}
.subtitle {
font-size: 1.6rem;
font-weight: 400;
color: var(--color-blue-scale-200);
}
.content {
overflow: auto;
font-size: 1.6rem;
max-height: calc(100vh - 40rem);
margin-top: 2rem;
}
.ui-button-group {
margin-top: 4rem;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<UiIcon
:class="textClass"
:icon="faXmark"
class="modal-close-icon"
@click="close"
/>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
const { textClass } = useContext(ColorContext);
const close = inject(IK_MODAL_CLOSE, undefined);
</script>
<style lang="postcss" scoped>
.modal-close-icon {
font-size: 2rem;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<component
:is="tag"
:class="[backgroundClass, { nested: isNested }]"
class="modal-container"
>
<header v-if="$slots.header" class="modal-header">
<slot name="header" />
</header>
<main v-if="$slots.default" class="modal-content">
<slot name="default" />
</main>
<footer v-if="$slots.footer" class="modal-footer">
<slot name="footer" />
</footer>
</component>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_NESTED } from "@/types/injection-keys";
import { inject, provide } from "vue";
const props = withDefaults(
defineProps<{
tag?: string;
color?: Color;
}>(),
{ tag: "div" }
);
defineSlots<{
header: () => any;
default: () => any;
footer: () => any;
}>();
const { backgroundClass } = useContext(ColorContext, () => props.color);
const isNested = inject(IK_MODAL_NESTED, false);
provide(IK_MODAL_NESTED, true);
</script>
<style lang="postcss" scoped>
.modal-container {
display: grid;
grid-template-rows: 1fr auto 1fr;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 20rem);
padding: 2rem;
gap: 1rem;
border-radius: 1rem;
font-size: 1.6rem;
&:not(.nested) {
min-width: 40rem;
box-shadow: var(--shadow-400);
}
&.nested {
margin-top: 2rem;
}
}
.modal-header {
grid-row: 1;
}
.modal-content {
text-align: center;
grid-row: 2;
padding: 2rem;
max-height: 75vh;
overflow: auto;
}
.modal-footer {
grid-row: 3;
align-self: end;
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="ui-modal" @click.self="close">
<slot />
</div>
</Teleport>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { useMagicKeys, useVModel, whenever } from "@vueuse/core/index";
import { provide } from "vue";
const props = defineProps<{
modelValue: boolean;
color?: Color;
}>();
const emit = defineEmits<{
(event: "update:modelValue", value: boolean): void;
}>();
const isOpen = useVModel(props, "modelValue", emit);
const close = () => (isOpen.value = false);
provide(IK_MODAL_CLOSE, close);
useContext(ColorContext, () => props.color);
const { escape } = useMagicKeys();
whenever(escape, () => close());
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background: rgba(26, 27, 56, 0.25);
flex-direction: column;
gap: 2rem;
font-size: 1.6rem;
font-weight: 400;
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<ModalContainer>
<template #header>
<ModalCloseIcon class="close-icon" />
</template>
<template #default>
<slot />
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
defineSlots<{
default: () => void;
}>();
</script>
<style lang="postcss" scoped>
.close-icon {
float: right;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<ModalContainer tag="form">
<template #header>
<div class="close-bar">
<ModalCloseIcon />
</div>
</template>
<template #default>
<UiIcon :class="textClass" :icon="icon" class="main-icon" />
<div v-if="$slots.title || $slots.subtitle" class="titles">
<UiTitle v-if="$slots.title" type="h4">
<slot name="title" />
</UiTitle>
<div v-if="$slots.subtitle" class="subtitle">
<slot name="subtitle" />
</div>
</div>
<div v-if="$slots.default">
<slot name="default" />
</div>
</template>
<template #footer>
<UiButtonGroup>
<slot name="buttons" />
</UiButtonGroup>
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{
icon?: IconDefinition;
}>();
const { textClass } = useContext(ColorContext);
defineSlots<{
title: () => void;
subtitle: () => void;
default: () => void;
buttons: () => void;
}>();
</script>
<style lang="postcss" scoped>
.close-bar {
text-align: right;
}
.main-icon {
font-size: 4.8rem;
margin-bottom: 2rem;
}
.titles {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.subtitle {
font-size: 1.6rem;
font-weight: 400;
color: var(--color-blue-scale-200);
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<ModalContainer tag="form">
<template #header>
<div :class="borderClass" class="title-bar">
<UiIcon :class="textClass" :icon="icon" />
<slot name="title" />
<ModalCloseIcon class="close-icon" />
</div>
</template>
<template #default>
<slot />
</template>
<template #footer>
<UiButtonGroup class="footer-buttons">
<slot name="buttons" />
</UiButtonGroup>
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
disabled?: boolean;
}>(),
{ disabled: undefined }
);
defineSlots<{
title: () => void;
default: () => void;
buttons: () => void;
}>();
const { textClass, borderClass } = useContext(ColorContext);
useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>
.title-bar {
display: flex;
border-bottom-width: 1px;
border-bottom-style: solid;
font-size: 2.4rem;
gap: 1rem;
padding-bottom: 1rem;
font-weight: 500;
align-items: center;
}
.close-icon {
margin-left: auto;
align-self: flex-start;
}
.footer-buttons {
justify-content: flex-end;
}
</style>

View File

@@ -1,6 +1,9 @@
<template>
<MenuItem
v-tooltip="!areAllSelectedVmsHalted && $t('selected-vms-in-execution')"
v-tooltip="
!areAllSelectedVmsHalted &&
$t(isSingleAction ? 'vm-is-running' : 'selected-vms-in-execution')
"
:busy="areSomeSelectedVmsCloning"
:disabled="isDisabled"
:icon="faCopy"
@@ -14,7 +17,7 @@
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCopy } from "@fortawesome/free-solid-svg-icons";
@@ -22,6 +25,7 @@ import { computed } from "vue";
const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
isSingleAction?: boolean;
}>();
const { getByOpaqueRef, isOperationPending } = useVmCollection();
@@ -36,7 +40,7 @@ const areAllSelectedVmsHalted = computed(
() =>
selectedVms.value.length > 0 &&
selectedVms.value.every(
(selectedVm) => selectedVm.power_state === POWER_STATE.HALTED
(selectedVm) => selectedVm.power_state === VM_POWER_STATE.HALTED
)
);

View File

@@ -1,48 +1,49 @@
<template>
<MenuItem
v-tooltip="areSomeVmsInExecution && $t('selected-vms-in-execution')"
:disabled="areSomeVmsInExecution"
:disabled="isDisabled"
:icon="faTrashCan"
@click="openDeleteModal"
>
{{ $t("delete") }}
</MenuItem>
<UiModal
v-if="isDeleteModalOpen"
:icon="faSatellite"
@close="closeDeleteModal"
>
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span :class="textClass">
{{ $t("n-vms", { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
</template>
<template #buttons>
<UiButton outlined @click="closeDeleteModal">
{{ $t("go-back") }}
</UiButton>
<UiButton @click="deleteVms">
{{ $t("delete-vms", { n: vmRefs.length }) }}
</UiButton>
</template>
<UiModal v-model="isDeleteModalOpen">
<ConfirmModalLayout :icon="faSatellite">
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span :class="textClass">
{{ $t("n-vms", { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
</template>
<template #buttons>
<UiButton outlined @click="closeDeleteModal">
{{ $t("go-back") }}
</UiButton>
<UiButton @click="deleteVms">
{{ $t("delete-vms", { n: vmRefs.length }) }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import { useContext } from "@/composables/context.composable";
import useModal from "@/composables/modal.composable";
import { ColorContext } from "@/context";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { faSatellite, faTrashCan } from "@fortawesome/free-solid-svg-icons";
@@ -65,7 +66,11 @@ const vms = computed<XenApiVm[]>(() =>
);
const areSomeVmsInExecution = computed(() =>
vms.value.some((vm) => vm.power_state !== POWER_STATE.HALTED)
vms.value.some((vm) => vm.power_state !== VM_POWER_STATE.HALTED)
);
const isDisabled = computed(
() => vms.value.length === 0 || areSomeVmsInExecution.value
);
const deleteVms = async () => {

View File

@@ -0,0 +1,95 @@
<template>
<MenuItem
v-tooltip="
!areAllVmsMigratable && $t('some-selected-vms-can-not-be-migrated')
"
:busy="isMigrating"
:disabled="isParentDisabled || !areAllVmsMigratable"
:icon="faRoute"
@click="openModal"
>
{{ $t("migrate") }}
</MenuItem>
<UiModal v-model="isModalOpen">
<FormModalLayout :disabled="isMigrating" @submit.prevent="handleMigrate">
<template #title>
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</template>
<div>
<FormInputWrapper :label="$t('select-destination-host')" light>
<FormSelect v-model="selectedHost">
<option :value="undefined">
{{ $t("select-destination-host") }}
</option>
<option
v-for="host in availableHosts"
:key="host.$ref"
:value="host"
>
{{ host.name_label }}
</option>
</FormSelect>
</FormInputWrapper>
</div>
<template #buttons>
<UiButton outlined @click="closeModal">
{{ isMigrating ? $t("close") : $t("cancel") }}
</UiButton>
<UiButton :busy="isMigrating" :disabled="!isValid" type="submit">
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</UiButton>
</template>
</FormModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useContext } from "@/composables/context.composable";
import useModal from "@/composables/modal.composable";
import { useVmMigration } from "@/composables/vm-migration.composable";
import { DisabledContext } from "@/context";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { faRoute } from "@fortawesome/free-solid-svg-icons";
const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
}>();
const isParentDisabled = useContext(DisabledContext);
const {
open: openModal,
isOpen: isModalOpen,
close: closeModal,
} = useModal({
onClose: () => (selectedHost.value = undefined),
});
const {
selectedHost,
availableHosts,
isValid,
migrate,
isMigrating,
areAllVmsMigratable,
} = useVmMigration(() => props.selectedRefs);
const handleMigrate = async () => {
try {
await migrate();
closeModal();
} catch (e) {
console.error("Error while migrating", e);
}
};
</script>

View File

@@ -100,7 +100,7 @@ import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { useXenApiStore } from "@/stores/xen-api.store";
import {
faCirclePlay,
@@ -136,16 +136,16 @@ const vmRefsWithPowerState = computed(() =>
const xenApi = useXenApiStore().getXapi();
const areVmsRunning = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.RUNNING)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.RUNNING)
);
const areVmsHalted = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.HALTED)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.HALTED)
);
const areVmsSuspended = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.SUSPENDED)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.SUSPENDED)
);
const areVmsPaused = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.PAUSED)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.PAUSED)
);
const areOperationsPending = (operation: VM_OPERATION | VM_OPERATION[]) =>
@@ -179,7 +179,7 @@ const areVmsBusyToForceShutdown = computed(() =>
areOperationsPending(VM_OPERATION.HARD_SHUTDOWN)
);
const getHostState = (host: XenApiHost) =>
isHostRunning(host) ? POWER_STATE.RUNNING : POWER_STATE.HALTED;
isHostRunning(host) ? VM_POWER_STATE.RUNNING : VM_POWER_STATE.HALTED;
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,52 @@
<template>
<MenuItem
:busy="areSomeVmsSnapshoting"
:disabled="isDisabled"
:icon="faCamera"
@click="handleSnapshot"
>
{{ $t("snapshot") }}
</MenuItem>
</template>
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCamera } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef, isOperationPending } = useVmCollection();
const vms = computed(() =>
props.vmRefs
.map((vmRef) => getByOpaqueRef(vmRef))
.filter((vm): vm is XenApiVm => vm !== undefined)
);
const areSomeVmsSnapshoting = computed(() =>
vms.value.some((vm) => isOperationPending(vm, VM_OPERATION.SNAPSHOT))
);
const isDisabled = computed(
() => vms.value.length === 0 || areSomeVmsSnapshoting.value
);
const handleSnapshot = () => {
const vmRefsToSnapshot = Object.fromEntries(
vms.value.map((vm) => [
vm.$ref,
`${vm.name_label}_${new Date().toISOString()}`,
])
);
return useXenApiStore().getXapi().vm.snapshot(vmRefsToSnapshot);
};
</script>
<style lang="postcss" scoped></style>

View File

@@ -11,6 +11,23 @@
</template>
<VmActionPowerStateItems :vm-refs="[vm.$ref]" />
</AppMenu>
<AppMenu v-if="vm !== undefined" placement="bottom-end" shadow>
<template #trigger="{ open, isOpen }">
<UiButton
:active="isOpen"
:icon="faEllipsisVertical"
@click="open"
transparent
class="more-actions-button"
v-tooltip="{
placement: 'left',
content: $t('more-actions'),
}"
/>
</template>
<VmActionCopyItem :selected-refs="[vm.$ref]" is-single-action />
<VmActionSnapshotItem :vm-refs="[vm.$ref]" />
</AppMenu>
</template>
</TitleBar>
</template>
@@ -21,11 +38,15 @@ import TitleBar from "@/components/TitleBar.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import {
faAngleDown,
faDisplay,
faEllipsisVertical,
faPowerOff,
} from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -40,3 +61,9 @@ const vm = computed(() =>
const name = computed(() => vm.value?.name_label);
</script>
<style lang="postcss">
.more-actions-button {
font-size: 1.2em;
}
</style>

View File

@@ -15,16 +15,12 @@
<VmActionPowerStateItems :vm-refs="selectedRefs" />
</template>
</MenuItem>
<MenuItem v-tooltip="$t('coming-soon')" :icon="faRoute">
{{ $t("migrate") }}
</MenuItem>
<VmActionMigrateItem :selected-refs="selectedRefs" />
<VmActionCopyItem :selected-refs="selectedRefs" />
<MenuItem v-tooltip="$t('coming-soon')" :icon="faEdit">
{{ $t("edit-config") }}
</MenuItem>
<MenuItem v-tooltip="$t('coming-soon')" :icon="faCamera">
{{ $t("snapshot") }}
</MenuItem>
<VmActionSnapshotItem :vm-refs="selectedRefs" />
<VmActionExportItem :vm-refs="selectedRefs" />
<VmActionDeleteItem :vm-refs="selectedRefs" />
</AppMenu>
@@ -35,18 +31,18 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
import VmActionDeleteItem from "@/components/vm/VmActionItems/VmActionDeleteItem.vue";
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
import VmActionMigrateItem from "@/components/vm/VmActionItems/VmActionMigrateItem.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useUiStore } from "@/stores/ui.store";
import {
faCamera,
faEdit,
faEllipsis,
faPowerOff,
faRoute,
} from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";

View File

@@ -10,8 +10,7 @@ export const useChartTheme = () => {
const getColors = () => ({
background: style.getPropertyValue("--background-color-primary"),
title: style.getPropertyValue("--color-blue-scale-100"),
subtitle: style.getPropertyValue("--color-blue-scale-300"),
text: style.getPropertyValue("--color-blue-scale-300"),
splitLine: style.getPropertyValue("--color-blue-scale-400"),
primary: style.getPropertyValue("--color-extra-blue-base"),
secondary: style.getPropertyValue("--color-orange-world-base"),
@@ -28,24 +27,10 @@ export const useChartTheme = () => {
backgroundColor: colors.value.background,
textStyle: {},
grid: {
top: 80,
top: 40,
left: 80,
right: 20,
},
title: {
textStyle: {
color: colors.value.title,
fontFamily: "Poppins, sans-serif",
fontWeight: 500,
fontSize: 20,
},
subtextStyle: {
color: colors.value.subtitle,
fontFamily: "Poppins, sans-serif",
fontWeight: 400,
fontSize: 14,
},
},
line: {
itemStyle: {
borderWidth: 2,
@@ -235,7 +220,7 @@ export const useChartTheme = () => {
},
axisLabel: {
show: true,
color: colors.value.subtitle,
color: colors.value.text,
},
splitLine: {
show: true,
@@ -295,7 +280,7 @@ export const useChartTheme = () => {
},
axisLabel: {
show: true,
color: colors.value.subtitle,
color: colors.value.text,
},
splitLine: {
show: true,
@@ -325,7 +310,7 @@ export const useChartTheme = () => {
left: "right",
top: "bottom",
textStyle: {
color: colors.value.subtitle,
color: colors.value.text,
},
},
tooltip: {

View File

@@ -0,0 +1,95 @@
import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useHostStore } from "@/stores/xen-api/host.store";
import type { XenApiPatch } from "@/types/xen-api";
import { type Pausable, useTimeoutPoll, watchArray } from "@vueuse/core";
import { computed, type MaybeRefOrGetter, reactive, toValue } from "vue";
export type XenApiPatchWithHostRefs = XenApiPatch & { $hostRefs: Set<string> };
type HostConfig = {
timeoutPoll: Pausable;
patches: XenApiPatch[];
isLoaded: boolean;
};
export const useHostPatches = (hosts: MaybeRefOrGetter<XenApiHost[]>) => {
const hostStore = useHostStore();
const configByHost = reactive(new Map<string, HostConfig>());
const fetchHostPatches = async (hostRef: XenApiHost["$ref"]) => {
if (!configByHost.has(hostRef)) {
return;
}
const config = configByHost.get(hostRef)!;
config.patches = await hostStore.fetchMissingPatches(hostRef);
config.isLoaded = true;
};
const registerHost = (hostRef: XenApiHost["$ref"]) => {
if (configByHost.has(hostRef)) {
return;
}
const timeoutPoll = useTimeoutPoll(() => fetchHostPatches(hostRef), 10000, {
immediate: true,
});
configByHost.set(hostRef, {
timeoutPoll,
patches: [],
isLoaded: false,
});
};
const unregisterHost = (hostRef: string) => {
configByHost.get(hostRef)?.timeoutPoll.pause();
configByHost.delete(hostRef);
};
watchArray(
() => toValue(hosts).map((host) => host.$ref),
(_n, _p, addedRefs, removedRefs) => {
addedRefs.forEach((ref) => registerHost(ref));
removedRefs?.forEach((ref) => unregisterHost(ref));
},
{ immediate: true }
);
const patches = computed(() => {
const records = new Map<string, XenApiPatchWithHostRefs>();
configByHost.forEach(({ patches }, hostRef) => {
patches.forEach((patch) => {
const record = records.get(patch.$id);
if (record !== undefined) {
return record.$hostRefs.add(hostRef);
}
records.set(patch.$id, {
...patch,
$hostRefs: new Set([hostRef]),
});
});
});
return Array.from(records.values());
});
const count = computed(() => patches.value.length);
const areAllLoaded = computed(() =>
Array.from(configByHost.values()).every((config) => config.isLoaded)
);
const areSomeLoaded = computed(
() =>
areAllLoaded.value ||
Array.from(configByHost.values()).some((config) => config.isLoaded)
);
return { patches, count, areAllLoaded, areSomeLoaded };
};

View File

@@ -1,16 +1,57 @@
# useModal composable
### Usage
#### API
`useModal<T>(options: ModalOptions)`
Type parameter:
- `T`: The type for the modal's payload.
Parameters:
- `options`: An optional object of type `ModalOptions`.
Returns an object with:
- `payload: ReadOnly<Ref<T | undefined>>`: The payload data of the modal. Mainly used if a single modal is used for
multiple items (typically with `v-for`)
- `isOpen: WritableComputedRef<boolean>`: A writable computed indicating if the modal is open or not.
- `open(currentPayload?: T)`: A function to open the modal and optionally set its payload.
- `close(force = false)`: A function to close the modal. If force is set to `true`, the modal will be closed without
calling the `confirmClose` callback.
#### Types
`ModalOptions`
An object type that accepts:
- `confirmClose?: () => boolean`: An optional callback that is called before the modal is closed. If this function
returns `false`, the modal will not be closed.
- `onClose?: () => void`: An optional callback that is called after the modal is closed.
### Example
```vue
<template>
<div v-for="item in items">
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
{{ item.name }}
<button @click="openRemoveModal(item)">Delete</button>
</div>
<UiModal v-if="isRemoveModalOpen">
Are you sure you want to delete {{ removeModalPayload.name }}
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
<UiModal v-model="isRemoveModalOpen">
<ModalContainer>
<template #header>
Are you sure you want to delete {{ removeModalPayload.name }}?
</template>
<template #footer>
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
</template>
</ModalContainer>
</UiModal>
</template>
@@ -22,7 +63,11 @@ const {
isOpen: isRemoveModalOpen,
open: openRemoveModal,
close: closeRemoveModal,
} = useModal();
} = useModal({
confirmClose: () =>
window.confirm("Are you sure you want to close this modal?"),
onClose: () => console.log("Modal closed"),
});
async function handleRemove() {
await removeItem(removeModalPayload.id);

View File

@@ -1,6 +1,11 @@
import { ref } from "vue";
import { computed, readonly, ref } from "vue";
export default function useModal<T>() {
type ModalOptions = {
confirmClose?: () => boolean;
onClose?: () => void;
};
export default function useModal<T>(options: ModalOptions = {}) {
const $payload = ref<T>();
const $isOpen = ref(false);
@@ -8,15 +13,35 @@ export default function useModal<T>() {
$isOpen.value = true;
$payload.value = payload;
};
const close = (force = false) => {
if (!force && options.confirmClose?.() === false) {
return;
}
if (options.onClose) {
options.onClose();
}
const close = () => {
$isOpen.value = false;
$payload.value = undefined;
};
const isOpen = computed({
get() {
return $isOpen.value;
},
set(value) {
if (value) {
open();
} else {
close();
}
},
});
return {
payload: $payload,
isOpen: $isOpen,
payload: readonly($payload),
isOpen,
open,
close,
};

View File

@@ -0,0 +1,82 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { castArray } from "lodash-es";
import type { MaybeRefOrGetter } from "vue";
import { computed, ref, toValue } from "vue";
export const useVmMigration = (
vmRefs: MaybeRefOrGetter<XenApiVm["$ref"] | XenApiVm["$ref"][]>
) => {
const $isMigrating = ref(false);
const selectedHost = ref<XenApiHost>();
const { getByOpaqueRef: getVm } = useVmCollection();
const { records: hosts } = useHostCollection();
const vms = computed(
() =>
castArray(toValue(vmRefs))
.map((vmRef) => getVm(vmRef))
.filter((vm) => vm !== undefined) as XenApiVm[]
);
const isMigrating = computed(
() =>
$isMigrating.value ||
vms.value.some((vm) =>
Object.values(vm.current_operations).some(
(operation) => operation === VM_OPERATION.POOL_MIGRATE
)
)
);
const availableHosts = computed(() => {
return hosts.value
.filter((host) => vms.value.some((vm) => vm.resident_on !== host.$ref))
.sort(sortRecordsByNameLabel);
});
const areAllVmsMigratable = computed(() =>
vms.value.every((vm) =>
vm.allowed_operations.includes(VM_OPERATION.POOL_MIGRATE)
)
);
const isValid = computed(
() =>
!isMigrating.value &&
vms.value.length > 0 &&
selectedHost.value !== undefined
);
const migrate = async () => {
if (!isValid.value) {
return;
}
try {
$isMigrating.value = true;
const hostRef = selectedHost.value!.$ref;
const xapi = useXenApiStore().getXapi();
await xapi.vm.migrate(
vms.value.map((vm) => vm.$ref),
hostRef
);
} finally {
$isMigrating.value = false;
}
};
return {
isMigrating,
availableHosts,
selectedHost,
areAllVmsMigratable,
isValid,
migrate,
};
};

View File

@@ -12,12 +12,12 @@ export type MixinAbstractConstructor<T = unknown> = abstract new (
export type MixinFunction<
T extends MixinConstructor | MixinAbstractConstructor = MixinConstructor,
R extends T = T & MixinConstructor
R extends T = T & MixinConstructor,
> = (Base: T) => R;
export type MixinReturnValue<
T extends MixinConstructor | MixinAbstractConstructor,
M extends MixinFunction<T, any>[]
M extends MixinFunction<T, any>[],
> = UnionToIntersection<
| T
| {
@@ -27,7 +27,7 @@ export type MixinReturnValue<
export default function mixin<
T extends MixinConstructor | MixinAbstractConstructor,
M extends MixinFunction<T, any>[]
M extends MixinFunction<T, any>[],
>(Base: T, ...mixins: M): MixinReturnValue<T, M> {
return mixins.reduce(
(mix, applyMixin) => applyMixin(mix),

View File

@@ -0,0 +1,493 @@
export enum TASK_ALLOWED_OPERATION {
CANCEL = "cancel",
DESTROY = "destroy",
}
export enum TASK_STATUS_TYPE {
CANCELLED = "cancelled",
CANCELLING = "cancelling",
FAILURE = "failure",
PENDING = "pending",
SUCCESS = "success",
}
export enum EVENT_OPERATION {
ADD = "add",
DEL = "del",
MOD = "mod",
}
export enum POOL_ALLOWED_OPERATION {
APPLY_UPDATES = "apply_updates",
CERT_REFRESH = "cert_refresh",
CLUSTER_CREATE = "cluster_create",
CONFIGURE_REPOSITORIES = "configure_repositories",
COPY_PRIMARY_HOST_CERTS = "copy_primary_host_certs",
DESIGNATE_NEW_MASTER = "designate_new_master",
EXCHANGE_CA_CERTIFICATES_ON_JOIN = "exchange_ca_certificates_on_join",
EXCHANGE_CERTIFICATES_ON_JOIN = "exchange_certificates_on_join",
GET_UPDATES = "get_updates",
HA_DISABLE = "ha_disable",
HA_ENABLE = "ha_enable",
SYNC_UPDATES = "sync_updates",
TLS_VERIFICATION_ENABLE = "tls_verification_enable",
}
export enum TELEMETRY_FREQUENCY {
DAILY = "daily",
MONTHLY = "monthly",
WEEKLY = "weekly",
}
export enum UPDATE_SYNC_FREQUENCY {
DAILY = "daily",
WEEKLY = "weekly",
}
export enum AFTER_APPLY_GUIDANCE {
RESTART_HOST = "restartHost",
RESTART_HVM = "restartHVM",
RESTART_PV = "restartPV",
RESTART_XAPI = "restartXAPI",
}
export enum UPDATE_AFTER_APPLY_GUIDANCE {
RESTART_HOST = "restartHost",
RESTART_HVM = "restartHVM",
RESTART_PV = "restartPV",
RESTART_XAPI = "restartXAPI",
}
export enum LIVEPATCH_STATUS {
OK = "ok",
OK_LIVEPATCH_COMPLETE = "ok_livepatch_complete",
OK_LIVEPATCH_INCOMPLETE = "ok_livepatch_incomplete",
}
export enum VM_POWER_STATE {
HALTED = "Halted",
PAUSED = "Paused",
RUNNING = "Running",
SUSPENDED = "Suspended",
}
export enum UPDATE_GUIDANCE {
REBOOT_HOST = "reboot_host",
REBOOT_HOST_ON_LIVEPATCH_FAILURE = "reboot_host_on_livepatch_failure",
RESTART_DEVICE_MODEL = "restart_device_model",
RESTART_TOOLSTACK = "restart_toolstack",
}
export enum ON_SOFTREBOOT_BEHAVIOR {
DESTROY = "destroy",
PRESERVE = "preserve",
RESTART = "restart",
SOFT_REBOOT = "soft_reboot",
}
export enum ON_NORMAL_EXIT {
DESTROY = "destroy",
RESTART = "restart",
}
export enum VM_OPERATION {
ASSERT_OPERATION_VALID = "assert_operation_valid",
AWAITING_MEMORY_LIVE = "awaiting_memory_live",
CALL_PLUGIN = "call_plugin",
CHANGING_DYNAMIC_RANGE = "changing_dynamic_range",
CHANGING_MEMORY_LIMITS = "changing_memory_limits",
CHANGING_MEMORY_LIVE = "changing_memory_live",
CHANGING_NVRAM = "changing_NVRAM",
CHANGING_SHADOW_MEMORY = "changing_shadow_memory",
CHANGING_SHADOW_MEMORY_LIVE = "changing_shadow_memory_live",
CHANGING_STATIC_RANGE = "changing_static_range",
CHANGING_VCPUS = "changing_VCPUs",
CHANGING_VCPUS_LIVE = "changing_VCPUs_live",
CHECKPOINT = "checkpoint",
CLEAN_REBOOT = "clean_reboot",
CLEAN_SHUTDOWN = "clean_shutdown",
CLONE = "clone",
COPY = "copy",
CREATE_TEMPLATE = "create_template",
CREATE_VTPM = "create_vtpm",
CSVM = "csvm",
DATA_SOURCE_OP = "data_source_op",
DESTROY = "destroy",
EXPORT = "export",
GET_BOOT_RECORD = "get_boot_record",
HARD_REBOOT = "hard_reboot",
HARD_SHUTDOWN = "hard_shutdown",
IMPORT = "import",
MAKE_INTO_TEMPLATE = "make_into_template",
METADATA_EXPORT = "metadata_export",
MIGRATE_SEND = "migrate_send",
PAUSE = "pause",
POOL_MIGRATE = "pool_migrate",
POWER_STATE_RESET = "power_state_reset",
PROVISION = "provision",
QUERY_SERVICES = "query_services",
RESUME = "resume",
RESUME_ON = "resume_on",
REVERT = "revert",
REVERTING = "reverting",
SEND_SYSRQ = "send_sysrq",
SEND_TRIGGER = "send_trigger",
SHUTDOWN = "shutdown",
SNAPSHOT = "snapshot",
SNAPSHOT_WITH_QUIESCE = "snapshot_with_quiesce",
START = "start",
START_ON = "start_on",
SUSPEND = "suspend",
UNPAUSE = "unpause",
UPDATE_ALLOWED_OPERATIONS = "update_allowed_operations",
}
export enum ON_CRASH_BEHAVIOUR {
COREDUMP_AND_DESTROY = "coredump_and_destroy",
COREDUMP_AND_RESTART = "coredump_and_restart",
DESTROY = "destroy",
PRESERVE = "preserve",
RENAME_RESTART = "rename_restart",
RESTART = "restart",
}
export enum DOMAIN_TYPE {
HVM = "hvm",
PV = "pv",
PVH = "pvh",
PV_IN_PVH = "pv_in_pvh",
UNSPECIFIED = "unspecified",
}
export enum TRISTATE_TYPE {
NO = "no",
UNSPECIFIED = "unspecified",
YES = "yes",
}
export enum VMPP_BACKUP_TYPE {
CHECKPOINT = "checkpoint",
SNAPSHOT = "snapshot",
}
export enum VMPP_BACKUP_FREQUENCY {
DAILY = "daily",
HOURLY = "hourly",
WEEKLY = "weekly",
}
export enum VMPP_ARCHIVE_FREQUENCY {
ALWAYS_AFTER_BACKUP = "always_after_backup",
DAILY = "daily",
NEVER = "never",
WEEKLY = "weekly",
}
export enum VMPP_ARCHIVE_TARGET_TYPE {
CIFS = "cifs",
NFS = "nfs",
NONE = "none",
}
export enum VMSS_FREQUENCY {
DAILY = "daily",
HOURLY = "hourly",
WEEKLY = "weekly",
}
export enum VMSS_TYPE {
CHECKPOINT = "checkpoint",
SNAPSHOT = "snapshot",
SNAPSHOT_WITH_QUIESCE = "snapshot_with_quiesce",
}
export enum VM_APPLIANCE_OPERATION {
CLEAN_SHUTDOWN = "clean_shutdown",
HARD_SHUTDOWN = "hard_shutdown",
SHUTDOWN = "shutdown",
START = "start",
}
export enum HOST_ALLOWED_OPERATION {
APPLY_UPDATES = "apply_updates",
EVACUATE = "evacuate",
POWER_ON = "power_on",
PROVISION = "provision",
REBOOT = "reboot",
SHUTDOWN = "shutdown",
VM_MIGRATE = "vm_migrate",
VM_RESUME = "vm_resume",
VM_START = "vm_start",
}
export enum LATEST_SYNCED_UPDATES_APPLIED_STATE {
NO = "no",
UNKNOWN = "unknown",
YES = "yes",
}
export enum HOST_DISPLAY {
DISABLED = "disabled",
DISABLE_ON_REBOOT = "disable_on_reboot",
ENABLED = "enabled",
ENABLE_ON_REBOOT = "enable_on_reboot",
}
export enum HOST_SCHED_GRAN {
CORE = "core",
CPU = "cpu",
SOCKET = "socket",
}
export enum NETWORK_OPERATION {
ATTACHING = "attaching",
}
export enum NETWORK_DEFAULT_LOCKING_MODE {
DISABLED = "disabled",
UNLOCKED = "unlocked",
}
export enum NETWORK_PURPOSE {
INSECURE_NBD = "insecure_nbd",
NBD = "nbd",
}
export enum VIF_OPERATION {
ATTACH = "attach",
PLUG = "plug",
UNPLUG = "unplug",
}
export enum VIF_LOCKING_MODE {
DISABLED = "disabled",
LOCKED = "locked",
NETWORK_DEFAULT = "network_default",
UNLOCKED = "unlocked",
}
export enum VIF_IPV4_CONFIGURATION_MODE {
NONE = "None",
STATIC = "Static",
}
export enum VIF_IPV6_CONFIGURATION_MODE {
NONE = "None",
STATIC = "Static",
}
export enum PIF_IGMP_STATUS {
DISABLED = "disabled",
ENABLED = "enabled",
UNKNOWN = "unknown",
}
export enum IP_CONFIGURATION_MODE {
DHCP = "DHCP",
NONE = "None",
STATIC = "Static",
}
export enum IPV6_CONFIGURATION_MODE {
AUTOCONF = "Autoconf",
DHCP = "DHCP",
NONE = "None",
STATIC = "Static",
}
export enum PRIMARY_ADDRESS_TYPE {
IPV4 = "IPv4",
IPV6 = "IPv6",
}
export enum BOND_MODE {
ACTIVE_BACKUP = "active-backup",
BALANCE_SLB = "balance-slb",
LACP = "lacp",
}
export enum STORAGE_OPERATION {
DESTROY = "destroy",
FORGET = "forget",
PBD_CREATE = "pbd_create",
PBD_DESTROY = "pbd_destroy",
PLUG = "plug",
SCAN = "scan",
UNPLUG = "unplug",
UPDATE = "update",
VDI_CLONE = "vdi_clone",
VDI_CREATE = "vdi_create",
VDI_DATA_DESTROY = "vdi_data_destroy",
VDI_DESTROY = "vdi_destroy",
VDI_DISABLE_CBT = "vdi_disable_cbt",
VDI_ENABLE_CBT = "vdi_enable_cbt",
VDI_INTRODUCE = "vdi_introduce",
VDI_LIST_CHANGED_BLOCKS = "vdi_list_changed_blocks",
VDI_MIRROR = "vdi_mirror",
VDI_RESIZE = "vdi_resize",
VDI_SET_ON_BOOT = "vdi_set_on_boot",
VDI_SNAPSHOT = "vdi_snapshot",
}
export enum SR_HEALTH {
HEALTHY = "healthy",
RECOVERING = "recovering",
}
export enum VDI_OPERATION {
BLOCKED = "blocked",
CLONE = "clone",
COPY = "copy",
DATA_DESTROY = "data_destroy",
DESTROY = "destroy",
DISABLE_CBT = "disable_cbt",
ENABLE_CBT = "enable_cbt",
FORCE_UNLOCK = "force_unlock",
FORGET = "forget",
GENERATE_CONFIG = "generate_config",
LIST_CHANGED_BLOCKS = "list_changed_blocks",
MIRROR = "mirror",
RESIZE = "resize",
RESIZE_ONLINE = "resize_online",
SET_ON_BOOT = "set_on_boot",
SNAPSHOT = "snapshot",
UPDATE = "update",
}
export enum VDI_TYPE {
CBT_METADATA = "cbt_metadata",
CRASHDUMP = "crashdump",
EPHEMERAL = "ephemeral",
HA_STATEFILE = "ha_statefile",
METADATA = "metadata",
PVS_CACHE = "pvs_cache",
REDO_LOG = "redo_log",
RRD = "rrd",
SUSPEND = "suspend",
SYSTEM = "system",
USER = "user",
}
export enum ON_BOOT {
PERSIST = "persist",
RESET = "reset",
}
export enum VBD_OPERATION {
ATTACH = "attach",
EJECT = "eject",
INSERT = "insert",
PAUSE = "pause",
PLUG = "plug",
UNPAUSE = "unpause",
UNPLUG = "unplug",
UNPLUG_FORCE = "unplug_force",
}
export enum VBD_TYPE {
CD = "CD",
DISK = "Disk",
FLOPPY = "Floppy",
}
export enum VBD_MODE {
RO = "RO",
RW = "RW",
}
export enum VTPM_OPERATION {
DESTROY = "destroy",
}
export enum PERSISTENCE_BACKEND {
XAPI = "xapi",
}
export enum CONSOLE_PROTOCOL {
RDP = "rdp",
RFB = "rfb",
VT100 = "vt100",
}
export enum CLS {
CERTIFICATE = "Certificate",
HOST = "Host",
POOL = "Pool",
PVS_PROXY = "PVS_proxy",
SR = "SR",
VDI = "VDI",
VM = "VM",
VMPP = "VMPP",
VMSS = "VMSS",
}
export enum TUNNEL_PROTOCOL {
GRE = "gre",
VXLAN = "vxlan",
}
export enum SRIOV_CONFIGURATION_MODE {
MANUAL = "manual",
MODPROBE = "modprobe",
SYSFS = "sysfs",
UNKNOWN = "unknown",
}
export enum PGPU_DOM0_ACCESS {
DISABLED = "disabled",
DISABLE_ON_REBOOT = "disable_on_reboot",
ENABLED = "enabled",
ENABLE_ON_REBOOT = "enable_on_reboot",
}
export enum ALLOCATION_ALGORITHM {
BREADTH_FIRST = "breadth_first",
DEPTH_FIRST = "depth_first",
}
export enum VGPU_TYPE_IMPLEMENTATION {
GVT_G = "gvt_g",
MXGPU = "mxgpu",
NVIDIA = "nvidia",
NVIDIA_SRIOV = "nvidia_sriov",
PASSTHROUGH = "passthrough",
}
export enum PVS_PROXY_STATUS {
CACHING = "caching",
INCOMPATIBLE_PROTOCOL_VERSION = "incompatible_protocol_version",
INCOMPATIBLE_WRITE_CACHE_MODE = "incompatible_write_cache_mode",
INITIALISED = "initialised",
STOPPED = "stopped",
}
export enum SDN_CONTROLLER_PROTOCOL {
PSSL = "pssl",
SSL = "ssl",
}
export enum VUSB_OPERATION {
ATTACH = "attach",
PLUG = "plug",
UNPLUG = "unplug",
}
export enum CLUSTER_OPERATION {
ADD = "add",
DESTROY = "destroy",
DISABLE = "disable",
ENABLE = "enable",
REMOVE = "remove",
}
export enum CLUSTER_HOST_OPERATION {
DESTROY = "destroy",
DISABLE = "disable",
ENABLE = "enable",
}
export enum CERTIFICATE_TYPE {
CA = "ca",
HOST = "host",
HOST_INTERNAL = "host_internal",
}

View File

@@ -296,7 +296,7 @@ export default class XenApi {
XenApiVm["$ref"],
XenApiVm["power_state"]
>;
type VmRefsToClone = Record<XenApiVm["$ref"], /* Cloned VM name */ string>;
type VmRefsWithNameLabel = Record<XenApiVm["$ref"], string>;
return {
delete: (vmRefs: VmRefs) =>
@@ -351,7 +351,7 @@ export default class XenApi {
)
);
},
clone: (vmRefsToClone: VmRefsToClone) => {
clone: (vmRefsToClone: VmRefsWithNameLabel) => {
const vmRefs = Object.keys(vmRefsToClone) as XenApiVm["$ref"][];
return Promise.all(
@@ -360,6 +360,26 @@ export default class XenApi {
)
);
},
migrate: (vmRefs: VmRefs, destinationHostRef: XenApiHost["$ref"]) => {
return Promise.all(
castArray(vmRefs).map((vmRef) =>
this.call("VM.pool_migrate", [
vmRef,
destinationHostRef,
{ force: "false" },
])
)
);
},
snapshot: (vmRefsToSnapshot: VmRefsWithNameLabel) => {
const vmRefs = Object.keys(vmRefsToSnapshot) as XenApiVm["$ref"][];
return Promise.all(
vmRefs.map((vmRef) =>
this.call("VM.snapshot", [vmRef, vmRefsToSnapshot[vmRef]])
)
);
},
};
}
}

View File

@@ -1,8 +1,46 @@
import type {
XEN_API_OBJECT_TYPES,
POWER_STATE,
ALLOCATION_ALGORITHM,
BOND_MODE,
DOMAIN_TYPE,
IP_CONFIGURATION_MODE,
IPV6_CONFIGURATION_MODE,
NETWORK_DEFAULT_LOCKING_MODE,
NETWORK_OPERATION,
NETWORK_PURPOSE,
ON_BOOT,
ON_CRASH_BEHAVIOUR,
ON_NORMAL_EXIT,
ON_SOFTREBOOT_BEHAVIOR,
PERSISTENCE_BACKEND,
PGPU_DOM0_ACCESS,
PIF_IGMP_STATUS,
PRIMARY_ADDRESS_TYPE,
SRIOV_CONFIGURATION_MODE,
TUNNEL_PROTOCOL,
UPDATE_GUIDANCE,
VBD_MODE,
VBD_OPERATION,
VBD_TYPE,
VDI_OPERATION,
VDI_TYPE,
VGPU_TYPE_IMPLEMENTATION,
VIF_IPV4_CONFIGURATION_MODE,
VIF_IPV6_CONFIGURATION_MODE,
VIF_LOCKING_MODE,
VIF_OPERATION,
VM_APPLIANCE_OPERATION,
VM_OPERATION,
} from "@/libs/xen-api/xen-api.utils";
VM_POWER_STATE,
VMPP_ARCHIVE_FREQUENCY,
VMPP_ARCHIVE_TARGET_TYPE,
VMPP_BACKUP_FREQUENCY,
VMPP_BACKUP_TYPE,
VMSS_FREQUENCY,
VMSS_TYPE,
VTPM_OPERATION,
VUSB_OPERATION,
} from "@/libs/xen-api/xen-api.enums";
import type { XEN_API_OBJECT_TYPES } from "@/libs/xen-api/xen-api.utils";
type TypeMapping = typeof XEN_API_OBJECT_TYPES;
export type ObjectType = keyof TypeMapping;
@@ -81,18 +119,236 @@ export interface XenApiSr extends XenApiRecord<"sr"> {
}
export interface XenApiVm extends XenApiRecord<"vm"> {
current_operations: Record<string, VM_OPERATION>;
guest_metrics: string;
metrics: XenApiVmMetrics["$ref"];
name_label: string;
name_description: string;
power_state: POWER_STATE;
resident_on: XenApiHost["$ref"];
HVM_boot_params: Record<string, string>;
HVM_boot_policy: string;
HVM_shadow_multiplier: number;
NVRAM: Record<string, string>;
PCI_bus: string;
PV_args: string;
PV_bootloader: string;
PV_bootloader_args: string;
PV_kernel: string;
PV_legacy_args: string;
PV_ramdisk: string;
VBDs: XenApiVbd["$ref"][];
VCPUs_at_startup: number;
VCPUs_max: number;
VCPUs_params: Record<string, string>;
VGPUs: XenApiVgpu["$ref"][];
VIFs: XenApiVif["$ref"][];
VTPMs: XenApiVtpm["$ref"][];
VUSBs: XenApiVusb["$ref"][];
actions_after_crash: ON_CRASH_BEHAVIOUR;
actions_after_reboot: ON_NORMAL_EXIT;
actions_after_shutdown: ON_NORMAL_EXIT;
actions_after_softreboot: ON_SOFTREBOOT_BEHAVIOR;
affinity: XenApiHost["$ref"];
allowed_operations: VM_OPERATION[];
appliance: XenApiVmAppliance["$ref"];
attached_PCIs: XenApiPci["$ref"][];
bios_strings: Record<string, string>;
blobs: Record<string, XenApiBlob["$ref"]>;
blocked_operations: Record<VM_OPERATION, string>;
children: XenApiVm["$ref"][];
consoles: XenApiConsole["$ref"][];
is_control_domain: boolean;
crash_dumps: XenApiCrashdump["$ref"][];
current_operations: Record<string, VM_OPERATION>;
domain_type: DOMAIN_TYPE;
domarch: string;
domid: number;
generation_id: string;
guest_metrics: XenApiVmGuestMetrics["$ref"];
ha_always_run: boolean;
ha_restart_priority: string;
hardware_platform_version: number;
has_vendor_device: boolean;
is_a_snapshot: boolean;
is_a_template: boolean;
VCPUs_at_startup: number;
is_control_domain: boolean;
is_default_template: boolean;
is_snapshot_from_vmpp: boolean;
is_vmss_snapshot: boolean;
last_boot_CPU_flags: Record<string, string>;
last_booted_record: string;
memory_dynamic_max: number;
memory_dynamic_min: number;
memory_overhead: number;
memory_static_max: number;
memory_static_min: number;
memory_target: number;
metrics: XenApiVmMetrics["$ref"];
name_description: string;
name_label: string;
order: number;
other_config: Record<string, string>;
parent: XenApiVm["$ref"];
pending_guidances: UPDATE_GUIDANCE[];
platform: Record<string, string>;
power_state: VM_POWER_STATE;
protection_policy: XenApiVmpp["$ref"];
recommendations: string;
reference_label: string;
requires_reboot: boolean;
resident_on: XenApiHost["$ref"];
scheduled_to_be_resident_on: XenApiHost["$ref"];
shutdown_delay: number;
snapshot_info: Record<string, string>;
snapshot_metadata: string;
snapshot_of: XenApiVm["$ref"];
snapshot_schedule: XenApiVmss["$ref"];
snapshot_time: string;
snapshots: XenApiVm["$ref"][];
start_delay: number;
suspend_SR: XenApiSr["$ref"];
suspend_VDI: XenApiVdi["$ref"];
tags: string[];
transportable_snapshot_id: string;
user_version: number;
version: number;
xenstore_data: Record<string, string>;
}
export interface XenApiVtpm extends XenApiRecord<"vtpm"> {
VM: XenApiVm["$ref"];
allowed_operations: VTPM_OPERATION[];
backend: XenApiVm["$ref"];
current_operations: Record<string, VTPM_OPERATION>;
is_protected: boolean;
is_unique: boolean;
persistence_backend: PERSISTENCE_BACKEND;
}
export interface XenApiVusb extends XenApiRecord<"vusb"> {
USB_group: XenApiUsbGroup["$ref"];
VM: XenApiVm["$ref"];
allowed_operations: VUSB_OPERATION[];
current_operations: Record<string, VUSB_OPERATION>;
currently_attached: boolean;
other_config: Record<string, string>;
}
export interface XenApiUsbGroup extends XenApiRecord<"usb_group"> {
PUSBs: XenApiPusb["$ref"][];
VUSBs: XenApiVusb["$ref"][];
name_description: string;
name_label: string;
other_config: Record<string, string>;
}
export interface XenApiPusb extends XenApiRecord<"pusb"> {
USB_group: XenApiUsbGroup["$ref"];
description: string;
host: XenApiHost["$ref"];
other_config: Record<string, string>;
passthrough_enabled: boolean;
path: string;
product_desc: string;
product_id: string;
serial: string;
speed: number;
vendor_desc: string;
vendor_id: string;
version: string;
}
export interface XenApiVgpu extends XenApiRecord<"vgpu"> {
GPU_group: XenApiGpuGroup["$ref"];
PCI: XenApiPci["$ref"];
VM: XenApiVm["$ref"];
compatibility_metadata: Record<string, string>;
currently_attached: boolean;
device: string;
extra_args: string;
other_config: Record<string, string>;
resident_on: XenApiPgpu["$ref"];
scheduled_to_be_resident_on: XenApiPgpu["$ref"];
type: XenApiVgpuType["$ref"];
}
export interface XenApiGpuGroup extends XenApiRecord<"gpu_group"> {
GPU_types: string[];
PGPUs: XenApiPgpu["$ref"][];
VGPUs: XenApiVgpu["$ref"][];
allocation_algorithm: ALLOCATION_ALGORITHM;
enabled_VGPU_types: XenApiVgpuType["$ref"][];
name_description: string;
name_label: string;
other_config: Record<string, string>;
supported_VGPU_types: XenApiVgpuType["$ref"][];
}
export interface XenApiPgpu extends XenApiRecord<"pgpu"> {
GPU_group: XenApiGpuGroup["$ref"];
PCI: XenApiPci["$ref"];
compatibility_metadata: Record<string, string>;
dom0_access: PGPU_DOM0_ACCESS;
enabled_VGPU_types: XenApiVgpuType["$ref"][];
host: XenApiHost["$ref"];
is_system_display_device: boolean;
other_config: Record<string, string>;
resident_VGPUs: XenApiVgpu["$ref"][];
supported_VGPU_max_capacities: Record<XenApiVgpuType["$ref"], number>;
supported_VGPU_types: XenApiVgpuType["$ref"][];
}
export interface XenApiVgpuType extends XenApiRecord<"vgpu_type"> {
VGPUs: XenApiVgpu["$ref"][];
compatible_types_in_vm: XenApiVgpuType["$ref"][];
enabled_on_GPU_groups: XenApiGpuGroup["$ref"][];
enabled_on_PGPUs: XenApiPgpu["$ref"][];
experimental: boolean;
framebuffer_size: number;
identifier: string;
implementation: VGPU_TYPE_IMPLEMENTATION;
max_heads: number;
max_resolution_x: number;
max_resolution_y: number;
model_name: string;
supported_on_GPU_groups: XenApiGpuGroup["$ref"][];
supported_on_PGPUs: XenApiPgpu["$ref"][];
vendor_name: string;
}
export interface XenApiVmAppliance extends XenApiRecord<"vm_appliance"> {
VMs: XenApiVm["$ref"][];
allowed_operations: VM_APPLIANCE_OPERATION[];
current_operations: Record<string, VM_APPLIANCE_OPERATION>;
name_description: string;
name_label: string;
}
export interface XenApiVmpp extends XenApiRecord<"vmpp"> {
VMs: XenApiVm["$ref"][];
alarm_config: Record<string, string>;
archive_frequency: VMPP_ARCHIVE_FREQUENCY;
archive_last_run_time: string;
archive_schedule: Record<string, string>;
archive_target_config: Record<string, string>;
archive_target_type: VMPP_ARCHIVE_TARGET_TYPE;
backup_frequency: VMPP_BACKUP_FREQUENCY;
backup_last_run_time: string;
backup_retention_value: number;
backup_schedule: Record<string, string>;
backup_type: VMPP_BACKUP_TYPE;
is_alarm_enabled: boolean;
is_archive_running: boolean;
is_backup_running: boolean;
is_policy_enabled: boolean;
name_description: string;
name_label: string;
recent_alerts: string[];
}
export interface XenApiVmss extends XenApiRecord<"vmss"> {
VMs: XenApiVm["$ref"][];
enabled: boolean;
frequency: VMSS_FREQUENCY;
last_run_time: string;
name_description: string;
name_label: string;
retained_snapshots: number;
schedule: Record<string, string>;
type: VMSS_TYPE;
}
export interface XenApiConsole extends XenApiRecord<"console"> {
@@ -131,6 +387,238 @@ export interface XenApiMessage<RelationType extends RawObjectType>
timestamp: string;
}
export interface XenApiVbd extends XenApiRecord<"vbd"> {
VDI: XenApiVdi["$ref"];
VM: XenApiVm["$ref"];
allowed_operations: VBD_OPERATION[];
bootable: boolean;
current_operations: Record<string, VBD_OPERATION>;
currently_attached: boolean;
device: string;
empty: boolean;
metrics: XenApiVbdMetrics["$ref"];
mode: VBD_MODE;
other_config: Record<string, string>;
qos_algorithm_params: Record<string, string>;
qos_algorithm_type: string;
qos_supported_algorithms: string[];
runtime_properties: Record<string, string>;
status_code: number;
status_detail: string;
storage_lock: boolean;
type: VBD_TYPE;
unpluggable: boolean;
userdevice: string;
}
export interface XenApiVbdMetrics extends XenApiRecord<"vbd_metrics"> {
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
}
export interface XenApiVdi extends XenApiRecord<"vdi"> {
SR: XenApiSr["$ref"];
VBDs: XenApiVbd["$ref"][];
allow_caching: boolean;
allowed_operations: VDI_OPERATION[];
cbt_enabled: boolean;
crash_dumps: XenApiCrashdump["$ref"][];
current_operations: Record<string, VDI_OPERATION>;
is_a_snapshot: boolean;
is_tools_iso: boolean;
location: string;
managed: boolean;
metadata_latest: boolean;
metadata_of_pool: XenApiPool["$ref"];
missing: boolean;
name_description: string;
name_label: string;
on_boot: ON_BOOT;
other_config: Record<string, string>;
parent: XenApiVdi["$ref"];
physical_utilisation: number;
read_only: boolean;
sharable: boolean;
sm_config: Record<string, string>;
snapshot_of: XenApiVdi["$ref"];
snapshot_time: string;
snapshots: XenApiVdi["$ref"][];
storage_lock: boolean;
tags: string[];
type: VDI_TYPE;
virtual_size: number;
xenstore_data: Record<string, string>;
}
export interface XenApiCrashdump extends XenApiRecord<"crashdump"> {
VDI: XenApiVdi["$ref"];
VM: XenApiVm["$ref"];
other_config: Record<string, string>;
}
export interface XenApiNetwork extends XenApiRecord<"network"> {
MTU: number;
PIFs: XenApiPif["$ref"][];
VIFs: XenApiVif["$ref"][];
allowed_operations: NETWORK_OPERATION[];
assigned_ips: Record<XenApiVif["$ref"], string>;
blobs: Record<string, XenApiBlob["$ref"]>;
bridge: string;
current_operations: Record<string, NETWORK_OPERATION>;
default_locking_mode: NETWORK_DEFAULT_LOCKING_MODE;
managed: boolean;
name_description: string;
name_label: string;
other_config: Record<string, string>;
purpose: NETWORK_PURPOSE[];
tags: string[];
}
export interface XenApiBlob extends XenApiRecord<"blob"> {
last_updated: string;
mime_type: string;
name_description: string;
name_label: string;
public: boolean;
size: number;
}
export interface XenApiVif extends XenApiRecord<"vif"> {
MAC: string;
MAC_autogenerated: boolean;
MTU: number;
VM: XenApiVm["$ref"];
allowed_operations: VIF_OPERATION[];
current_operations: Record<string, VIF_OPERATION>;
currently_attached: boolean;
device: string;
ipv4_addresses: string[];
ipv4_allowed: string[];
ipv4_configuration_mode: VIF_IPV4_CONFIGURATION_MODE;
ipv4_gateway: string;
ipv6_addresses: string[];
ipv6_allowed: string[];
ipv6_configuration_mode: VIF_IPV6_CONFIGURATION_MODE;
ipv6_gateway: string;
locking_mode: VIF_LOCKING_MODE;
metrics: XenApiVifMetrics["$ref"];
network: XenApiNetwork["$ref"];
other_config: Record<string, string>;
qos_algorithm_params: Record<string, string>;
qos_algorithm_type: string;
qos_supported_algorithms: string[];
runtime_properties: Record<string, string>;
status_code: number;
status_detail: string;
}
export interface XenApiVifMetrics extends XenApiRecord<"vif_metrics"> {
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
}
export interface XenApiPif extends XenApiRecord<"pif"> {
DNS: string;
IP: string;
IPv6: string[];
MAC: string;
MTU: number;
PCI: XenApiPci["$ref"];
VLAN: number;
VLAN_master_of: XenApiVlan["$ref"];
VLAN_slave_of: XenApiVlan["$ref"][];
bond_master_of: XenApiBond["$ref"][];
bond_slave_of: XenApiBond["$ref"];
capabilities: string[];
currently_attached: boolean;
device: string;
disallow_unplug: boolean;
gateway: string;
host: XenApiHost["$ref"];
igmp_snooping_status: PIF_IGMP_STATUS;
ip_configuration_mode: IP_CONFIGURATION_MODE;
ipv6_configuration_mode: IPV6_CONFIGURATION_MODE;
ipv6_gateway: string;
managed: boolean;
management: boolean;
metrics: XenApiPifMetrics["$ref"];
netmask: string;
network: XenApiNetwork["$ref"];
other_config: Record<string, string>;
physical: boolean;
primary_address_type: PRIMARY_ADDRESS_TYPE;
properties: Record<string, string>;
sriov_logical_PIF_of: XenApiNetworkSriov["$ref"][];
sriov_physical_PIF_of: XenApiNetworkSriov["$ref"][];
tunnel_access_PIF_of: XenApiTunnel["$ref"][];
tunnel_transport_PIF_of: XenApiTunnel["$ref"][];
}
export interface XenApiNetworkSriov extends XenApiRecord<"network_sriov"> {
configuration_mode: SRIOV_CONFIGURATION_MODE;
logical_PIF: XenApiPif["$ref"];
physical_PIF: XenApiPif["$ref"];
requires_reboot: boolean;
}
export interface XenApiVlan extends XenApiRecord<"vlan"> {
other_config: Record<string, string>;
tag: number;
tagged_PIF: XenApiPif["$ref"];
untagged_PIF: XenApiPif["$ref"];
}
export interface XenApiTunnel extends XenApiRecord<"tunnel"> {
access_PIF: XenApiPif["$ref"];
other_config: Record<string, string>;
protocol: TUNNEL_PROTOCOL;
status: Record<string, string>;
transport_PIF: XenApiPif["$ref"];
}
export interface XenApiPci extends XenApiRecord<"pci"> {
class_name: string;
dependencies: XenApiPci["$ref"][];
device_name: string;
driver_name: string;
host: XenApiHost["$ref"];
other_config: Record<string, string>;
pci_id: string;
subsystem_device_name: string;
subsystem_vendor_name: string;
vendor_name: string;
}
export interface XenApiPifMetrics extends XenApiRecord<"pif_metrics"> {
carrier: boolean;
device_id: string;
device_name: string;
duplex: boolean;
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
pci_bus_path: string;
speed: number;
vendor_id: string;
vendor_name: string;
}
export interface XenApiBond extends XenApiRecord<"bond"> {
auto_update_mac: boolean;
links_up: number;
master: XenApiPif["$ref"];
mode: BOND_MODE;
other_config: Record<string, string>;
primary_slave: XenApiPif["$ref"];
properties: Record<string, string>;
slaves: XenApiPif["$ref"][];
}
export type XenApiEvent<
RelationType extends ObjectType,
XRecord extends ObjectTypeToRecord<RelationType>,

View File

@@ -40,6 +40,7 @@ export const XEN_API_OBJECT_TYPES = {
vm: "VM",
vmpp: "VMPP",
vmss: "VMSS",
vm_appliance: "VM_appliance",
vm_guest_metrics: "VM_guest_metrics",
vm_metrics: "VM_metrics",
vusb: "VUSB",
@@ -62,6 +63,7 @@ export const XEN_API_OBJECT_TYPES = {
subject: "subject",
task: "task",
tunnel: "tunnel",
vtpm: "VTPM",
} as const;
export const rawTypeToType = <RawType extends RawObjectType>(
@@ -72,28 +74,6 @@ export const typeToRawType = <Type extends ObjectType>(
type: Type
): TypeToRawType<Type> => XEN_API_OBJECT_TYPES[type];
export enum POWER_STATE {
RUNNING = "Running",
PAUSED = "Paused",
HALTED = "Halted",
SUSPENDED = "Suspended",
}
export enum VM_OPERATION {
START = "start",
START_ON = "start_on",
RESUME = "resume",
UNPAUSE = "unpause",
CLONE = "clone",
SHUTDOWN = "shutdown",
CLEAN_SHUTDOWN = "clean_shutdown",
HARD_SHUTDOWN = "hard_shutdown",
CLEAN_REBOOT = "clean_reboot",
HARD_REBOOT = "hard_reboot",
PAUSE = "pause",
SUSPEND = "suspend",
}
export const buildXoObject = <T extends XenApiRecord<ObjectType>>(
record: RawXenApiRecord<T>,
params: { opaqueRef: T["$ref"] }

View File

@@ -27,10 +27,12 @@
"cancel": "Cancel",
"change-state": "Change state",
"click-to-display-alarms": "Click to display alarms:",
"confirm-delete": "You're about to delete {0}",
"close": "Close",
"coming-soon": "Coming soon!",
"community": "Community",
"community-name": "{name} community",
"confirm-cancel": "Are you sure you want to cancel?",
"confirm-delete": "You're about to delete {0}",
"console": "Console",
"console-unavailable": "Console unavailable",
"copy": "Copy",
@@ -42,9 +44,9 @@
"descending": "descending",
"description": "Description",
"display": "Display",
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"documentation": "Documentation",
"documentation-name": "{name} documentation",
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"edit-config": "Edit config",
"error-no-data": "Error, can't collect data.",
"error-occurred": "An error has occurred",
@@ -73,9 +75,12 @@
"following-hosts-unreachable": "The following hosts are unreachable",
"force-reboot": "Force reboot",
"force-shutdown": "Force shutdown",
"fullscreen": "Fullscreen",
"fullscreen-leave": "Leave fullscreen",
"go-back": "Go back",
"here": "Here",
"hosts": "Hosts",
"keep-me-logged": "Keep me logged in",
"language": "Language",
"last-week": "Last week",
"learn-more": "Learn more",
@@ -83,27 +88,32 @@
"loading-hosts": "Loading hosts…",
"log-out": "Log out",
"login": "Login",
"more-actions": "More actions",
"migrate": "Migrate",
"migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
"n-hosts-awaiting-patch": "{n} host is awaiting this patch | {n} hosts are awaiting this patch",
"n-missing": "{n} missing",
"n-vms": "1 VM | {n} VMs",
"name": "Name",
"network": "Network",
"network-download": "Download",
"network-throughput": "Network throughput",
"network-upload": "Upload",
"new-features-are-coming": "New features are coming soon!",
"news": "News",
"news-name": "{name} news",
"new-features-are-coming": "New features are coming soon!",
"no-alarm-triggered": "No alarm triggered",
"no-tasks": "No tasks",
"not-found": "Not found",
"object": "Object",
"object-not-found": "Object {id} can't be found…",
"on-object": "on {object}",
"open-in-new-window": "Open in new window",
"open-console-in-new-tab": "Open console in new tab",
"or": "Or",
"page-not-found": "This page is not to be found…",
"password": "Password",
"password-invalid": "Password invalid",
"patches": "Patches",
"pause": "Pause",
"please-confirm": "Please confirm",
"pool-cpu-usage": "Pool CPU Usage",
@@ -127,11 +137,14 @@
},
"resume": "Resume",
"save": "Save",
"select-destination-host": "Select a destination host",
"selected-vms-in-execution": "Some selected VMs are running",
"send-ctrl-alt-del": "Send Ctrl+Alt+Del",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"shutdown": "Shutdown",
"snapshot": "Snapshot",
"some-selected-vms-can-not-be-migrated": "Some selected VMs can't be migrated",
"sort-by": "Sort by",
"stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage",
@@ -164,6 +177,7 @@
"vcpus": "vCPUs",
"vcpus-used": "vCPUs used",
"version": "Version",
"vm-is-running": "The VM is running",
"vms": "VMs",
"xo-lite-under-construction": "XOLite is under construction"
}

View File

@@ -25,12 +25,14 @@
"back-pool-dashboard": "Revenez au tableau de bord de votre pool",
"backup": "Sauvegarde",
"cancel": "Annuler",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"change-state": "Changer l'état",
"click-to-display-alarms": "Cliquer pour afficher les alarmes :",
"close": "Fermer",
"coming-soon": "Bientôt disponible !",
"community": "Communauté",
"community-name": "Communauté {name}",
"confirm-cancel": "Êtes-vous sûr de vouloir annuler ?",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"console": "Console",
"console-unavailable": "Console indisponible",
"copy": "Copier",
@@ -42,9 +44,9 @@
"descending": "descendant",
"description": "Description",
"display": "Affichage",
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"documentation": "Documentation",
"documentation-name": "Documentation {name}",
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"edit-config": "Modifier config",
"error-no-data": "Erreur, impossible de collecter les données.",
"error-occurred": "Une erreur est survenue",
@@ -73,9 +75,12 @@
"following-hosts-unreachable": "Les hôtes suivants sont inaccessibles",
"force-reboot": "Forcer le redémarrage",
"force-shutdown": "Forcer l'arrêt",
"fullscreen": "Plein écran",
"fullscreen-leave": "Quitter plein écran",
"go-back": "Revenir en arrière",
"here": "Ici",
"hosts": "Hôtes",
"keep-me-logged": "Rester connecté",
"language": "Langue",
"last-week": "Semaine dernière",
"learn-more": "En savoir plus",
@@ -83,27 +88,32 @@
"loading-hosts": "Chargement des hôtes…",
"log-out": "Se déconnecter",
"login": "Connexion",
"more-actions": "Plus d'actions",
"migrate": "Migrer",
"migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
"n-hosts-awaiting-patch": "{n} hôte attend ce patch | {n} hôtes attendent ce patch",
"n-missing": "{n} manquant | {n} manquants",
"n-vms": "1 VM | {n} VMs",
"name": "Nom",
"network": "Réseau",
"network-download": "Descendant",
"network-throughput": "Débit du réseau",
"network-upload": "Montant",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"news": "Actualités",
"news-name": "Actualités {name}",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"no-alarm-triggered": "Aucune alarme déclenchée",
"no-tasks": "Aucune tâche",
"not-found": "Non trouvé",
"object": "Objet",
"object-not-found": "L'objet {id} est introuvable…",
"on-object": "sur {object}",
"open-in-new-window": "Ouvrir dans une nouvelle fenêtre",
"open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
"or": "Ou",
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"patches": "Patches",
"pause": "Pause",
"please-confirm": "Veuillez confirmer",
"pool-cpu-usage": "Utilisation CPU du Pool",
@@ -127,11 +137,14 @@
},
"resume": "Reprendre",
"save": "Enregistrer",
"select-destination-host": "Sélectionnez un hôte de destination",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"shutdown": "Arrêter",
"snapshot": "Instantané",
"some-selected-vms-can-not-be-migrated": "Certaines VMs sélectionnées ne peuvent pas être migrées",
"sort-by": "Trier par",
"stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée",
@@ -164,6 +177,7 @@
"vcpus": "vCPUs",
"vcpus-used": "vCPUs utilisés",
"version": "Version",
"vm-is-running": "La VM est en cours d'exécution",
"vms": "VMs",
"xo-lite-under-construction": "XOLite est en construction"
}

View File

@@ -1,7 +1,7 @@
import { useBreakpoints, useColorMode } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { useRoute } from "vue-router";
import { useRoute, useRouter } from "vue-router";
export const useUiStore = defineStore("ui", () => {
const currentHostOpaqueRef = ref();
@@ -14,8 +14,15 @@ export const useUiStore = defineStore("ui", () => {
const isMobile = computed(() => !isDesktop.value);
const router = useRouter();
const route = useRoute();
const hasUi = computed(() => route.query.ui !== "0");
const hasUi = computed<boolean>({
get: () => route.query.ui !== "0",
set: (value: boolean) => {
void router.replace({ query: { ui: value ? undefined : "0" } });
},
});
return {
colorMode,

View File

@@ -2,7 +2,7 @@ import XapiStats from "@/libs/xapi-stats";
import XenApi from "@/libs/xen-api/xen-api";
import { useLocalStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
const HOST_URL = import.meta.env.PROD
? window.origin
@@ -17,16 +17,24 @@ enum STATUS {
export const useXenApiStore = defineStore("xen-api", () => {
const xenApi = new XenApi(HOST_URL);
const xapiStats = new XapiStats(xenApi);
const currentSessionId = useLocalStorage<string | undefined>(
const storedSessionId = useLocalStorage<string | undefined>(
"sessionId",
undefined
);
const currentSessionId = ref(storedSessionId.value);
const rememberMe = useLocalStorage("rememberMe", false);
const status = ref(STATUS.DISCONNECTED);
const isConnected = computed(() => status.value === STATUS.CONNECTED);
const isConnecting = computed(() => status.value === STATUS.CONNECTING);
const getXapi = () => xenApi;
const getXapiStats = () => xapiStats;
watchEffect(() => {
storedSessionId.value = rememberMe.value
? currentSessionId.value
: undefined;
});
const connect = async (username: string, password: string) => {
status.value = STATUS.CONNECTING;
@@ -63,7 +71,7 @@ export const useXenApiStore = defineStore("xen-api", () => {
async function disconnect() {
await xenApi.disconnect();
currentSessionId.value = null;
currentSessionId.value = undefined;
status.value = STATUS.DISCONNECTED;
}

View File

@@ -38,7 +38,7 @@ type StoreToRefs<SS extends Store<any, any, any, any>> = ToRefs<
type Output<
S extends StoreDefinition<any, any, any, any>,
Defer extends boolean
Defer extends boolean,
> = Omit<S, keyof StoreToRefs<S> | IgnoredProperties> &
StoreToRefs<S> &
(Defer extends true
@@ -54,7 +54,7 @@ export const createUseCollection = <
infer A
>
? Store<Id, S, G, A>
: never
: never,
>(
useStore: SD
) => {

View File

@@ -4,6 +4,7 @@ import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { useHostMetricsStore } from "@/stores/xen-api/host-metrics.store";
import type { XenApiPatch } from "@/types/xen-api";
import { defineStore } from "pinia";
import { computed } from "vue";
@@ -42,10 +43,36 @@ export const useHostStore = defineStore("xen-api-host", () => {
});
}) as GetStats<XenApiHost>;
const fetchMissingPatches = async (
hostRef: XenApiHost["$ref"]
): Promise<XenApiPatch[]> => {
const xenApiStore = useXenApiStore();
const rawPatchesAsString = await xenApiStore
.getXapi()
.call<string>("host.call_plugin", [
hostRef,
"updater.py",
"check_update",
{},
]);
const rawPatches = JSON.parse(rawPatchesAsString) as Omit<
XenApiPatch,
"$id"
>[];
return rawPatches.map((rawPatch) => ({
...rawPatch,
$id: `${rawPatch.name}-${rawPatch.version}`,
}));
};
return {
...context,
runningHosts,
getStats,
fetchMissingPatches,
};
});

View File

@@ -3,8 +3,10 @@ import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-s
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import type { VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import {
type VM_OPERATION,
VM_POWER_STATE,
} from "@/libs/xen-api/xen-api.enums";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { useHostStore } from "@/stores/xen-api/host.store";
@@ -35,7 +37,7 @@ export const useVmStore = defineStore("xen-api-vm", () => {
};
const runningVms = computed(() =>
records.value.filter((vm) => vm.power_state === POWER_STATE.RUNNING)
records.value.filter((vm) => vm.power_state === VM_POWER_STATE.RUNNING)
);
const recordsByHostRef = computed(() => {

View File

@@ -9,6 +9,7 @@
prop('error').type('string').widget(),
prop('help').type('string').widget().preset('256 by default'),
prop('disabled').type('boolean').widget().ctx(),
prop('light').bool().widget(),
slot().help('Contains the input'),
]"
>

View File

@@ -16,8 +16,6 @@ type LinearChartData = {
```vue-template
<LinearChart
title="Chart title"
subtitle="Chart subtitle"
:data="data"
/>
```

View File

@@ -1,8 +1,6 @@
<template>
<ComponentStory
:params="[
prop('title').preset('Chart title').widget(),
prop('subtitle').preset('Here is a subtitle').widget(),
prop('data')
.preset(data)
.required()
@@ -58,8 +56,6 @@ const data: LinearChartData = [
const presets = {
"Network bandwidth": {
props: {
title: "Network bandwidth",
subtitle: "Last week",
"value-formatter": byteFormatter,
"max-value": 500000000,
data: [

View File

@@ -0,0 +1,11 @@
```vue-template
<UiModal v-model="isOpen">
<BasicModalLayout>
Here is a basic modal...
</BasicModalLayout>
</UiModal>
```
```vue-script
const { isOpen } = useModal();
```

View File

@@ -0,0 +1,22 @@
<template>
<ComponentStory
v-slot="{ settings }"
:params="[
slot(),
setting('defaultSlotContent').preset('Modal content').widget(text()),
]"
>
<BasicModalLayout>
{{ settings.defaultSlotContent }}
</BasicModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import { setting, slot } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,21 @@
```vue-template
<UiModal v-model="isOpen">
<ConfirmModalLayout :icon="faShip">
<template #title>Do you confirm?</template>
<template #subtitle>You should be sure about this</template>
<template #buttons>
<UiButton outlined @click="close">I prefer not</UiButton>
<UiButton @click="accept">Yes, I'm sure!</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
```
```vue-script
const { isOpen, close } = useModal();
const accept = async () => {
// do something
close();
}
```

View File

@@ -1,45 +1,32 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
colorProp(),
iconProp(),
event('close').preset(close),
slot('default'),
slot('title'),
slot('subtitle'),
slot('icon'),
slot('default'),
slot('buttons').help('Meant to receive UiButton components'),
setting('title').preset('Modal Title').widget(),
setting('subtitle').preset('Modal Subtitle').widget(),
]"
v-slot="{ properties, settings }"
>
<UiButton type="button" @click="open">Open Modal</UiButton>
<UiModal v-bind="properties" v-if="isOpen">
<ConfirmModalLayout v-bind="properties">
<template #title>{{ settings.title }}</template>
<template #subtitle>{{ settings.subtitle }}</template>
<template #buttons>
<UiButton @click="close">Discard</UiButton>
<UiButton outlined>Discard</UiButton>
<UiButton>Go</UiButton>
</template>
</UiModal>
</ConfirmModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import {
colorProp,
event,
iconProp,
setting,
slot,
} from "@/libs/story/story-param";
const { open, close, isOpen } = useModal();
import { iconProp, setting, slot } from "@/libs/story/story-param";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,25 @@
```vue-template
<UiModal v-model="isOpen">
<FormModalLayout :icon="faShip" @submit.prevent="handleSubmit">
<template #title>Migrate 3 VMs/template>
<template #default>
<!-- Form content goes here... -->
</template>
<template #buttons>
<UiButton outlined @click="close">Cancel</UiButton>
<UiButton type="submit">Migrate 3 VMs</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
```
```vue-script
const { isOpen, close } = useModal();
const handleSubmit = async () => {
// Handling form submission...
close();
}
```

View File

@@ -0,0 +1,54 @@
<template>
<ComponentStory
v-slot="{ properties }"
:params="[iconProp(), slot('title'), slot('default'), slot('buttons')]"
>
<FormModalLayout :icon="faRoute" v-bind="properties">
<template #title>Migrate 3 VMs</template>
<div>
<FormInputWrapper
label="Select a destination host"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
<FormInputWrapper
label="Select a migration network (optional)"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
<FormInputWrapper
help="Individual selection for each VDI is not available on multiple VMs migration."
label="Select a destination SR"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
</div>
<template #buttons>
<UiButton outlined>Cancel</UiButton>
<UiButton>Migrate 3 VMs</UiButton>
</template>
</FormModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { iconProp, slot } from "@/libs/story/story-param";
import { faRoute } from "@fortawesome/free-solid-svg-icons";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,19 @@
A basic modal container containing 3 slots: `header`, `default` and `footer`.
Tag will be `div` by default but can be changed with the `tag` prop.
Color can be changed with the `color` prop.
To keep the content centered vertically, header and footer will always have the same height.
Modal content has an max height + overflow to prevent the modal growing out of the screen.
Modal containers can be nested.
```vue-template
<ModalContainer>
<template #header>Header</template>
<template #default>Content</template>
<template #header>Footer</template>
</ModalContainer>
```

View File

@@ -0,0 +1,52 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
prop('tag').str().default('div').widget(),
colorProp(),
slot('header'),
slot(),
slot('footer'),
setting('headerSlotContent')
.preset('Header')
.widget(text())
.help('Content for default slot'),
setting('defaultSlotContent')
.preset('Content')
.widget(text())
.help('Content for default slot'),
setting('footerSlotContent')
.preset('Footer')
.widget(text())
.help('Content for default slot'),
setting('showNested')
.preset(false)
.widget(boolean())
.help('Show nested modal'),
]"
>
<ModalContainer v-bind="properties">
<template #header>
{{ settings.headerSlotContent }}
</template>
<template #default>
{{ settings.defaultSlotContent }}
<ModalContainer v-if="settings.showNested" color="error">
Nested modal
</ModalContainer>
</template>
<template #footer>
{{ settings.footerSlotContent }}
</template>
</ModalContainer>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import { colorProp, prop, setting, slot } from "@/libs/story/story-param";
import { boolean, text } from "@/libs/story/story-widget";
</script>

View File

@@ -0,0 +1,21 @@
This component only handle the modal backdrop and content positioning.
You can use any pre-made layouts, create your own or use the `ModalContainer` component.
It is meant to be used with `useModal` composable.
```vue-template
<button @click="open">Delete all items</button>
<UiModal v-model="isOpen">
<ModalContainer...>
<!-- <ConfirmModalLayout ...> (Or you can use a pre-made layout) -->
</UiModal>
```
```vue-script
import { faRemove } from "@fortawesome/free-solid-svg-icons";
import { useModal } from "@composable/modal.composable";
const { open, close, isOpen } = useModal().
```

View File

@@ -0,0 +1,24 @@
<template>
<ComponentStory
:params="[
model()
.required()
.type('boolean')
.help('Whether the modal is opened or not'),
colorProp().ctx(),
slot().help('Place your ModalContainer here'),
]"
>
<button type="button" @click="open">Open modal</button>
<UiModal v-model="isOpen" />
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { colorProp, model, slot } from "@/libs/story/story-param";
const { isOpen, open } = useModal();
</script>

View File

@@ -2,9 +2,9 @@
<ComponentStory
:params="[
prop('state')
.enum(...Object.values(POWER_STATE))
.enum(...Object.values(VM_POWER_STATE))
.required()
.preset(POWER_STATE.RUNNING)
.preset(VM_POWER_STATE.RUNNING)
.widget(),
]"
v-slot="{ properties }"
@@ -17,7 +17,7 @@
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import { prop } from "@/libs/story/story-param";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,19 +0,0 @@
```vue-template
<button @click="open">Delete all items</button>
<UiModal v-if="isOpen" @close="close" :icon="faRemove">
<template #title>You are about to delete 12 items</template>
<template #subtitle>They'll be gone forever</template>
<template #buttons>
<UiButton @click="delete" color="error">Yes, delete</UiButton>
<UiButton @click="close">Cancel</UiButton>
</template>
</UiModal>
```
```vue-script
import { faRemove } from "@fortawesome/free-solid-svg-icons";
import { useModal } from "@composable/modal.composable";
const { open, close, isOpen } = useModal().
```

View File

@@ -0,0 +1,5 @@
export enum UiCardTitleLevel {
Title,
Subtitle,
SubtitleWithUnderline,
}

View File

@@ -47,3 +47,7 @@ export const IK_BUTTON_GROUP_TRANSPARENT = Symbol() as InjectionKey<
export const IK_CARD_GROUP_VERTICAL = Symbol() as InjectionKey<boolean>;
export const IK_INPUT_ID = Symbol() as InjectionKey<ComputedRef<string>>;
export const IK_MODAL_CLOSE = Symbol() as InjectionKey<() => void>;
export const IK_MODAL_NESTED = Symbol() as InjectionKey<boolean>;

View File

@@ -21,3 +21,19 @@ export interface XenApiAlarm<RelationType extends RawObjectType>
triggerLevel: number;
type: XenApiAlarmType;
}
export type XenApiPatch = {
$id: string;
name: string;
description: string;
license: string;
release: string;
size: number;
url: string;
version: string;
changelog: {
date: number;
description: string;
author: string;
};
};

View File

@@ -3,7 +3,7 @@
<UiCardGroup>
<PoolDashboardStatus />
<PoolDashboardAlarms class="alarms" />
<UiCardComingSoon title="Patches" />
<PoolDashboardHostsPatches />
</UiCardGroup>
<UiCardGroup>
<UiCardGroup>
@@ -36,12 +36,12 @@ import PoolDashboardTasks from "@/components/pool/dashboard/PoolDashboardTasks.v
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuProvisioning from "@/components/pool/dashboard/PoolDashboardCpuProvisioning.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardHostsPatches from "@/components/pool/dashboard/PoolDashboardHostsPatches.vue";
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import UiCardComingSoon from "@/components/ui/UiCardComingSoon.vue";
import UiCardGroup from "@/components/ui/UiCardGroup.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";

View File

@@ -12,7 +12,6 @@
:available-filters="filters"
:available-sorts="filters"
:collection="vms"
id-property="$ref"
>
<template #head-row>
<ColumnHeader :icon="faPowerOff" />
@@ -23,7 +22,15 @@
<td>
<PowerStateIcon :state="vm.power_state" />
</td>
<td>{{ vm.name_label }}</td>
<td>
<div class="vm-name">
<UiSpinner
v-if="isMigrating(vm)"
v-tooltip="'This VM is being migrated'"
/>
{{ vm.name_label }}
</div>
</td>
<td>{{ vm.name_description }}</td>
</template>
</CollectionTable>
@@ -36,11 +43,14 @@ import ColumnHeader from "@/components/ColumnHeader.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { vTooltip } from "@/directives/tooltip.directive";
import { VM_OPERATION, VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { Filters } from "@/types/filter";
import { faPowerOff } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
@@ -52,7 +62,7 @@ const { t } = useI18n();
const titleStore = usePageTitleStore();
titleStore.setTitle(t("vms"));
const { records: vms } = useVmCollection();
const { records: vms, isOperationPending } = useVmCollection();
const { isMobile, isDesktop } = storeToRefs(useUiStore());
const filters: Filters = {
@@ -62,17 +72,26 @@ const filters: Filters = {
label: t("power-state"),
icon: faPowerOff,
type: "enum",
choices: Object.values(POWER_STATE),
choices: Object.values(VM_POWER_STATE),
},
};
const selectedVmsRefs = ref([]);
titleStore.setCount(() => selectedVmsRefs.value.length);
const isMigrating = (vm: XenApiVm) =>
isOperationPending(vm, VM_OPERATION.POOL_MIGRATE);
</script>
<style lang="postcss" scoped>
.pool-vms-view {
overflow: auto;
}
.vm-name {
display: inline-flex;
align-items: center;
gap: 1rem;
}
</style>

View File

@@ -110,19 +110,19 @@ const template = computed(() => {
]"
>
<${componentName} v-bind="properties"${
slotsNames.length > 0
? `>\n ${slotsNames
.map((name) =>
name === "default"
? `{{ settings.${camel(name)}SlotContent }}`
: `<template #${name}>{{ settings.${camel(
name
)}SlotContent }}</template>`
)
.join("\n ")}
slotsNames.length > 0
? `>\n ${slotsNames
.map((name) =>
name === "default"
? `{{ settings.${camel(name)}SlotContent }}`
: `<template #${name}>{{ settings.${camel(
name
)}SlotContent }}</template>`
)
.join("\n ")}
</${componentName}>`
: ` />`
}
: ` />`
}
</ComponentStory>
</template>

Some files were not shown because too many files have changed in this diff Show More