Compare commits

..

36 Commits

Author SHA1 Message Date
All-Ki
591c552e3e fix(qa-test): update engine version to requirements 2026-08-26 11:50:49 +02:00
Bastien Nollet
9cdaf6bb41 feat: technical release (#10305)
* feat(@xen-orchestra/async-map): 0.1.4

* feat(xo-common): 0.11.0

* feat(xen-api): 4.9.0

* feat(@vates/types): 1.30.2

* feat(@xen-orchestra/acl): 1.6.0

* feat(@xen-orchestra/xapi): 8.10.3

* feat(@xen-orchestra/backups): 0.74.0

* feat(@xen-orchestra/backups-cli): 1.1.14

* feat(@xen-orchestra/disk-cli): 2.2.1

* feat(@xen-orchestra/immutable-backups): 3.0.5

* feat(@xen-orchestra/web-core): 0.59.0

* feat(@xen-orchestra/mixins): 0.20.2

* feat(@xen-orchestra/proxy): 0.31.7

* feat(@xen-orchestra/proxy-cli): 0.3.3

* feat(@xen-orchestra/qa-test): 1.2.0

* feat(@xen-orchestra/rest-api): 0.38.0

* feat(@xen-orchestra/upload-ova): 0.1.9

* feat(@xen-orchestra/web): 0.58.0

* feat(xo-acl-resolver): 0.5.4

* feat(xo-cli): 0.32.4

* feat(xo-server): 5.207.3

* feat(xo-server-audit): 0.15.2

* feat(xo-server-ipmi-sensors): 2.1.0

* feat(xo-server-netbox): 1.13.2

* feat(xo-server-sdn-controller): 1.4.1

* feat(xo-web): 5.202.0

* chore(CHANGELOG): update next
2026-08-25 16:19:35 +02:00
Florent BEAUCHAMP
9b4e1aee3e fix(xo-server): task controller leaking memory (#10237) 2026-08-25 15:42:12 +02:00
Thomas Moraine
ab7b2fc7a9 docs(xo): refine page titles and navigation structure (#10276)
* docs(xo): refine page titles and navigation structure

Make page titles clearer and more consistent with their content and
purpose. Reorder some pages (and sections) and align their sidebar labels with
their titles to make the documentation easier to navigate and
understand.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

* docs(sidebars.ts): move discover xo category back to where it was

Applying the change suggested by olivierlambert.
The 'Discover Xen Orchestra' category now follows
the 'Getting started' category.
The 'architecture' page/ 'How XO 6 is organized' stays
at the end of the Getting Started category.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

* docs(xo): specify navbar titles

Applying changes suggested by olivierlambert.

Some titles in the navigation bar were made too long,
in an attempt to make them identical to the titles in the corresponding doc pages.
Having different titles in the navbar and in the actual page
is not a defect, and can indeed help readers scan the
document structure quicker.
As a result, we're (re-)adding sidebar_label metadata
to specify a navbar title where the page title was too long.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

* docs(xo): revert individual page renames

Applying changes suggested by olivierlambert, following,
the reasoning he explained in the pull request.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

* docs(intro_support.md) change title to 'support'

Apply change suggested by olivierlambert.
'Support' instead of 'Support options' is shorter and
carries the same meaning overall.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

* docs(xo): update outdated internal link labels

A few commits ago, some page titles were changed.

This did not break any internal links, but it left some link labels outdated. This commit updates those labels to match the current page titles.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

* docs: convert page titles to sentence case

Applying changes suggested by olivierlambert

Some page titles were still using title case, even though we use sentence case throughout the XO documentation.

This commit updates those titles to follow the same convention.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

* docs(xo): switch gerund titles to noun forms

Some page titles used gerunds (the -ing form), while others used nouns or noun phrases, resulting in an inconsistent style.

This commit changes the gerund titles to noun-based titles for consistency.

The "Troubleshooting" title is kept as-is because it is a noun, despite ending in -ing.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>

---------

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>
2026-08-25 14:23:34 +02:00
Thierry Goettelmann
9cdd8e9d45 chore(web-core,web,lite): enforce .ts extension in imports (#10299) 2026-08-25 11:01:00 +02:00
Sandrine Barrucand
14e5352a40 feat(xo6/vm): add guest tools status (#10077) 2026-08-25 10:40:50 +02:00
Marie Garde
d955ffc4d1 feat(xo6): add smart reboot host button (#10177)
* feat(xo6): reset smart reboot branch ok

* Last returns

* Remove route map

* rebase ok

* add check onConfirm smart reboot
2026-08-25 09:56:16 +02:00
spacotte-vates
f812eaaeb6 feat(backup): implement synchronized snapshot option back (#10136)
Adds a synchronizedSnapshot backup setting. When enabled, all of a job's VM snapshots are taken up front in a batch before any transfer begins, and each VM's backup then reuses its pre-taken snapshot instead of snapshotting again.

Settings:
synchronizedSnapshot:

    false (default)
    true (synchronize every VM in the job)
    <tag> (synchronize only VMs carrying that tag, others are backed up normally).

snapshotConcurrency: max concurrent snapshots during the batch phase (default 2).

A batch member that fails to open is silently dropped from synchronization and backed up normally.

Unit tested where applicable and e2e tests added in qa-test.

Checkbox added in xo5 backup jobs to synchronize snapshots, tags not supported yet.
2026-08-24 17:58:03 +02:00
amouillard-vates
814bc96a8b feat(xo6): sr dedicated page general tab (#10100)
* feat(xo6): sr dedicated page general tab

* Remove dev routes

* Update CHANGELOG.unreleased.md

* Rename sr usage warning

* Rename vdi allocated space warning

* Remove useless variable

* Remove useless destructuring

* Remove useless destructuring

* Use UiInfo component and normalize margins

* Change variable name

* Add pool/host breadcrumb options

* Normalize fromContext with vdi page

* SR space unwritable alert

* Side panel space card info message

* Usage warning color

* Fix PR comments

* Add sr uuid resolver

* Fix CI

* Update getters name

* Full width UiInfo

* Consistent srUsagePercentage check

* Remove superfluous destructuration

* Reuse existing SR_SCOPE_TYPE

* Remove dev routes

* CSS nesting

* Fix PR comments

* Dev routes + UiPanelCard

* Move getSrPageLocation
2026-08-24 11:13:13 +02:00
Alina D
9ef53890b8 feat(xo-lite): add vdi page (#10269) 2026-08-21 17:31:45 +02:00
spacotte-vates
8b4505a2d5 fix(xo-server): changing vif mac adress destroy traffic rules (#10284) 2026-08-21 17:15:48 +02:00
amouillard-vates
a68c9534e9 fix(lite): add hasChildren prop to xo-lite tree view elements (#10244) 2026-08-21 14:59:23 +02:00
Bastien Nollet
46b0dc67bb chore(backups): use set_memory_limits instead of convoluted code (#10286) 2026-08-21 14:36:22 +02:00
Thierry Goettelmann
249137793f fix(web-core,web,lite): align side panel card spacing with the design system (#10279) 2026-08-20 12:03:44 +02:00
amouillard-vates
96e7e23f40 feat(xo-lite): enable scan PIFs button on host network page (#10147) 2026-08-19 14:01:04 +02:00
Anupam Mediratta
6c3e3e781d fix(backups): upgrade tar to 7.5.19 (CVE-2026-59873) (#10159) 2026-08-19 10:42:23 +02:00
Marie Garde
bcb0a7041f feat(xo6): add force reboot host button (#10175)
* manual rebase

* returns from stack PR

* rebase ok

* returns from PR

* fix import

* remove unescessary params

* remove reoute map

* Last return
2026-08-18 11:48:42 +02:00
Alina D
db0d009d5b feat(xo6/pool): add forget action (#10130) 2026-08-18 11:17:18 +02:00
Elise-F
de1c231873 feat(lite): add create network and network bonded actions (#10145)
* feat(lite): add create network and network bonded actions

* update changelog file

* remove vlan required and info message

* feedback number one

* feedback number two
2026-08-18 10:49:27 +02:00
Marie Garde
4235644950 feat(xo6): add reboot host button (#10141)
* continuefeat(xo6/host): add reboot host button

* fix fr.json

* Add loader on evacuate and reboot

* add check message evacuate VMs

* Fix message

* Returns from PR

* Remove vm migrate from host operations

* Change accent modal

* Rebase master ok

* Fix rebase
2026-08-17 09:40:40 +02:00
Marie Garde
7942d6a84a feat(xo6): add disable host and evacuate VMs (#10090)
* feat(xo6): add disable and enable host button

* Add button enable disable

* remove route map file

* Refacto for PR

* Add modal

* refacto for PR

* fix i18n

* fix route map

* change modale to VtsActionModale

* refacto for MR

* fix type VtsActionModal

* refacto for MR

* add enable disable un treeview

* refacto treeview

* wip

* add shutdown and start button and job

* Update changelog and remove route map

* wip i18n

* tests ok

* fix build

* refacto for mr

* refacto for PR

* add check pending action

* add check host not halted

* feat(xo6): add disable and enable host button

* Add button enable disable

* Refacto for PR

* Add modal

* refacto for PR

* change key i18n

* change modale to VtsActionModale

* refacto for MR

* fix type VtsActionModal

* refacto for MR

* refacto treeview

* wip

* Update changelog and remove route map

* wip

* wip forget

* test and refacto for PR

* fix error rebase

* fx rebase missing

* fix rebase

* fix rebase

* wip

* add shutdown and start button and job

* tests ok

* refacto for mr

* add check host not halted

* wip

* rebase for PR

* fx rebase missing

* wip

* change order button

* wip

* wip forget

* wip

* button and job ok

* add modal disable host and evacuate vm but tests not done

* refacto for PR

* fix fr json and route map

* fix rebase

* fix PR

* remove route map

* last fix

* Returns from PR

* Returns from PR

* Add ? on confirm i18n

* Fix job disable and enable running message

* Fix enable job

* Fix enable job

* Returns from PR

* Fix post rebase

* Fix post rebase

* Fix post rebase

* fix from rebase

* Returns from PR

* Returns from PR

* rebase ok
2026-08-17 08:33:33 +02:00
amouillard-vates
c5faec4cf6 fix(lite): remove multi-selection from pool networks table (#10268) 2026-08-14 15:39:33 +02:00
Thomas Moraine
c9149618f8 docs(xo): rename security tab into traffic rules (#10267)
The Pools -> Security tab in Xen Orchestra was
recently renamed as 'Traffic rules'.
This commit updates the XO documentation (screenshots included) to reflect that change.

Signed-off-by: Thomas Moraine <thomas.moraine@vates.tech>
2026-08-14 13:42:38 +02:00
Elise-F
438415a882 feat(xo6/traffic rules): added the error message related to the method and updated UI (#10202) 2026-08-14 10:41:45 +02:00
OlivierFL
ed78cc10cf feat(xo-6/vif): add VIF name in head bar on VIF detail page (#10252) 2026-08-14 09:05:02 +02:00
Marie Garde
1c18215bd1 feat(xo6): add host action button forget (#10089)
* feat(xo6): add disable and enable host button

* Add button enable disable

* remove route map file

* Refacto for PR

* Add modal

* refacto for PR

* change key i18n

* fix i18n

* fix route map

* change modale to VtsActionModale

* refacto for MR

* fix type VtsActionModal

* refacto for MR

* add enable disable un treeview

* refacto treeview

* wip

* add shutdown and start button and job

* Update changelog and remove route map

* wip i18n

* tests ok

* fix build

* refacto for mr

* refacto for PR

* add check pending action

* add check host not halted

* feat(xo6): add disable and enable host button

* Add button enable disable

* Refacto for PR

* Add modal

* refacto for PR

* change key i18n

* change modale to VtsActionModale

* refacto for MR

* fix type VtsActionModal

* refacto for MR

* refacto treeview

* wip

* Update changelog and remove route map

* refacto for mr

* wip

* add forget button host

* update button for menuitem

* wip forget

* test and refacto for PR

* fix error rebase

* fx rebase missing

* add changelog

* fix rebase

* fix rebase

* Add ? on confirm i18n

* Returns from PR

* rebase overlay ok

* fix changelog

* Add redirect if still on host page after forget

* Add redirect after delete
2026-08-14 09:00:50 +02:00
Pierre Donias
7cdb792bfe fix(xo-web/vm/console): make collapsed header state reliable (#10007)
The console tab kept its own `minimalLayout` state to track whether the page header was collapsed, separate from the VM page's `collapsedHeader`. The two desynced because the tab remounts on navigation while the VM page stays mounted.

Fix:
- Single source of truth `collapsedHeader` in the parent index.js
- Pass it to the child tab-console.js along with `setCollapsedHeader`
- Replace the `_toggleHeader` toggle with an explicit `_setCollapsedHeader(value)` setter
- Remove the duplicate local `minimalLayout` state that could get out of sync and use `collapsedHeader` instead
- Force off on VM shutdown (console no longer displayed) and on unmount (user changed page)
2026-08-13 15:15:25 +02:00
Grandalf
0601716fa4 fix(backups): return backups repository info one by one instead of erroring on first failure (#10205) 2026-08-13 15:09:53 +02:00
spacotte-vates
d53d2fc1c2 fix(xo-server): fix target not found error for warm migration (#10210) 2026-08-13 09:56:10 +02:00
Bruno Verachten
11ff21097f docs(snapshots): note that reverting restores VM metadata too (#10196)
Surfaced via https://xcp-ng.org/forum/topic/12148.
2026-08-12 17:39:33 +02:00
Greg-hub-dev
828c15cbb4 docs(backups.md) : Delete future RAM backup feature details (#10223) 2026-08-12 15:03:33 +02:00
Grandalf
798732e62a fix(async-map): allow return type to be different than input type (#10227) 2026-08-12 10:50:44 +02:00
Mathieu
1bfcc50b23 fix(xen-api): update platform for cores-per-socket update (#10201)
XO-2814
2026-08-10 15:52:07 +02:00
Olivier Lambert
c5d791a7de docs: full documentation revamp (#10212) 2026-08-07 16:36:09 +02:00
Grandalf
3293e69cc2 refactor(xo-server): remove hrp dependency (#10038)
replace http-request-plus external depedency by a a native wrapper around fetch
2026-08-07 11:58:20 +02:00
Grandalf
a0193085f5 feat(ipmi): add get sensors route (#10003) 2026-08-07 11:38:28 +02:00
1160 changed files with 15845 additions and 9169 deletions

View File

@@ -85,6 +85,21 @@ module.exports = {
XO_LITE_GIT_HEAD: true,
},
rules: {
'import/extensions': [
'error',
'ignorePackages',
{
pattern: { ts: 'always', vue: 'always' },
checkTypeImports: true,
// '@core/*' and '@/*' look like scoped packages to the plugin, which 'ignorePackages' would skip
pathGroupOverrides: [
{ pattern: '@core/packages/*', action: 'ignore' }, // barrel files
{ pattern: '@core/icons', action: 'ignore' },
{ pattern: '@core/**', action: 'enforce' },
{ pattern: '@/**', action: 'enforce' },
],
},
],
'import/order': [
'error',
{

View File

@@ -5,3 +5,7 @@
# Generated files
/@xen-orchestra/web/src/route-map.d.ts
/@xen-orchestra/lite/src/route-map.d.ts
# Docusaurus MDX content: prettier's markdown parser corrupts JSX
# comments ({/* … */} becomes {/_ … _/}) and other MDX constructs
/docs/docs/**/*.md

View File

@@ -24,7 +24,7 @@
},
"dependencies": {
"@vates/multi-key-map": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.3",
"@xen-orchestra/async-map": "^0.1.4",
"@xen-orchestra/log": "^0.7.2",
"ensure-array": "^1.0.0"
},

View File

@@ -21,11 +21,11 @@
"dependencies": {
"@vates/async-each": "^1.0.3",
"@vates/read-chunk": "^1.2.1",
"@xen-orchestra/async-map": "^0.1.3",
"@xen-orchestra/async-map": "^0.1.4",
"@xen-orchestra/disk-transform": "^1.3.3",
"@xen-orchestra/log": "^0.7.2",
"promise-toolbox": "^0.21.0",
"xen-api": "^4.8.0"
"xen-api": "^4.9.0"
},
"devDependencies": {
"tap": "^18.7.0",

View File

@@ -1,7 +1,7 @@
{
"name": "@vates/types",
"private": false,
"version": "1.30.1",
"version": "1.30.2",
"main": "./dist/index.mjs",
"exports": {
".": {

View File

@@ -328,10 +328,11 @@ export type XoApp = {
xo: Record<XoBackupRepository['id'], XoConfigBackupArchive[]>
pool: Record<XoBackupRepository['id'], Record<XoPool['id'], XoPoolBackupArchive[]>>
}>
/** `null` when the listing of a backup repository failed */
listVmBackupsNg(
backupRepositoryIds: XoBackupRepository['id'][],
opts?: { _forceRefresh?: boolean; vmId: XoVm['id'] }
): Promise<Record<XoBackupRepository['id'], Record<XoVm['id'], XoVmBackupArchive[]>>>
opts?: { _forceRefresh?: boolean; vmId?: XoVm['id'] }
): Promise<Record<XoBackupRepository['id'], Record<XoVm['id'], XoVmBackupArchive[]> | null>>
pingRemote(id: XoBackupRepository['id']): Promise<{ success: true }>
/** Allow to add a new server in the DB (XCP-ng/XenServer) */
registerXenServer(

View File

@@ -6,7 +6,7 @@
"main": "dist/index.mjs",
"name": "@xen-orchestra/acl",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/acl",
"version": "1.5.0",
"version": "1.6.0",
"license": "AGPL-3.0-or-later",
"private": false,
"type": "module",
@@ -30,7 +30,7 @@
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@vates/types": "^1.30.1",
"@vates/types": "^1.30.2",
"rimraf": "^6.0.1",
"typescript": "~5.6",
"typescript-eslint": "^8.61.0"

View File

@@ -13,11 +13,11 @@ const wrapCall = (fn, arg, thisArg) => {
*
* WARNING: Does not handle plain objects
*
* @template Item,This
* @template Item, Result, [This=Iterable<Item>]
* @param {Iterable<Item>} iterable
* @param {(this: This, item: Item) => (Item | PromiseLike<Item>)} mapFn
* @param {This} [thisArg]
* @returns {Promise<Item[]>}
* @param {(this: This, item: Item) => (Result | PromiseLike<Result>)} mapFn
* @param {This} [thisArg] defaults to `iterable`
* @returns {Promise<Result[]>}
*/
exports.asyncMap = function asyncMap(iterable, mapFn, thisArg = iterable) {
return Promise.all(Array.from(iterable, mapFn, thisArg))
@@ -26,11 +26,11 @@ exports.asyncMap = function asyncMap(iterable, mapFn, thisArg = iterable) {
/**
* Like `asyncMap` but wait for all promises to settle before rejecting
*
* @template Item,This
* @template Item, Result, [This=Iterable<Item>]
* @param {Iterable<Item>} iterable
* @param {(this: This, item: Item) => (Item | PromiseLike<Item>)} mapFn
* @param {This} [thisArg]
* @returns {Promise<Item[]>}
* @param {(this: This, item: Item) => (Result | PromiseLike<Result>)} mapFn
* @param {This} [thisArg] defaults to `iterable`
* @returns {Promise<Result[]>}
*/
exports.asyncMapSettled = function asyncMapSettled(iterable, mapFn, thisArg = iterable) {
return new Promise((resolve, reject) => {

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/async-map",
"version": "0.1.3",
"version": "0.1.4",
"license": "ISC",
"description": "Promise.all + map for all iterables",
"keywords": [

View File

@@ -21,7 +21,7 @@
"tar-stream": "^3.1.8",
"@types/tar-stream": "^3.1.4",
"tmp": "^0.2.1",
"@vates/types": "^1.30.1"
"@vates/types": "^1.30.2"
},
"dependencies": {
"@xen-orchestra/log": "^0.7.1",

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@vates/async-each": "^1.0.3",
"@xen-orchestra/async-map": "^0.1.3",
"@xen-orchestra/backups": "^0.73.10",
"@xen-orchestra/async-map": "^0.1.4",
"@xen-orchestra/backups": "^0.74.0",
"@xen-orchestra/fs": "^4.9.3",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
@@ -28,7 +28,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.1.13",
"version": "1.1.14",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -16,42 +16,15 @@ import { toQcow2Stream } from '@xen-orchestra/qcow2'
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
const orderedMemoryLimits = ['memory_static_min', 'memory_dynamic_min', 'memory_dynamic_max', 'memory_static_max']
// The dynamic memory range MUST respect this inequality at any moment: static_min <= dynamic_min <= dynamic_max <= static_max.
// We must update these properties in the right order to avoid XAPI error.
// The order depends on the values. It can be an increase, a decrease or a mix of both, so any order could be required.
// VM.set_memory_limits sets all four fields atomically, so there is no intermediate state for XAPI to reject
export async function updateMemoryFields(xapi, targetVm, vmRecord) {
const memoryValues = {}
for (const key of orderedMemoryLimits) {
memoryValues[key] = {
currentValue: targetVm[key],
newValue: vmRecord[key] ?? targetVm[key],
}
}
const staticMin = vmRecord.memory_static_min ?? targetVm.memory_static_min
const staticMax = vmRecord.memory_static_max ?? targetVm.memory_static_max
const dynamicMin = vmRecord.memory_dynamic_min ?? targetVm.memory_dynamic_min
const dynamicMax = vmRecord.memory_dynamic_max ?? targetVm.memory_dynamic_max
while (await updateNextMemoryField(xapi, memoryValues, targetVm.$ref)) {
/* execute until all memory fields are updated */
}
}
// Update one more memory field if needed, then return a boolean describing if a field was updated or if all fields are up to date
async function updateNextMemoryField(xapi, memoryValues, vmRef) {
for (let i = 0; i < orderedMemoryLimits.length; i++) {
const currentField = memoryValues[orderedMemoryLimits[i]]
const nextField = i === orderedMemoryLimits.length - 1 ? undefined : memoryValues[orderedMemoryLimits[i + 1]]
if (
currentField.newValue !== currentField.currentValue &&
(nextField === undefined || currentField.newValue <= nextField.currentValue)
) {
// no need to check that previousField.currentValue <= currentField.newValue, as we can deduce it
await xapi.setField('VM', vmRef, orderedMemoryLimits[i], currentField.newValue)
await xapi.barrier()
currentField.currentValue = currentField.newValue
return true
}
}
return false
await xapi.call('VM.set_memory_limits', targetVm.$ref, staticMin, staticMax, dynamicMin, dynamicMax)
}
export async function exportIncrementalVm(

View File

@@ -1,122 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { updateMemoryFields } from './_incrementalVm.mjs'
const MOCK_REF = 'mockRef'
function mockXapiVm(initialMemoryValues) {
const res = {
...initialMemoryValues,
updates: [],
barrier: () => {},
setField: (type, ref, field, value) => {
assert(type === 'VM')
assert(ref === MOCK_REF)
res.updates.push({ field, value })
res[field] = value
assert(res.memory_static_min <= res.memory_dynamic_min)
assert(res.memory_dynamic_min <= res.memory_dynamic_max)
assert(res.memory_dynamic_max <= res.memory_static_max)
},
}
return res
}
const tests = [
{
label: 'handles a memory increase',
targetVm: {
memory_static_min: 2,
memory_dynamic_min: 4,
memory_dynamic_max: 4,
memory_static_max: 8,
},
vmRecord: {
memory_static_min: 16,
memory_dynamic_min: 16,
memory_dynamic_max: 16,
memory_static_max: 16,
},
expectedUpdates: [
{ field: 'memory_static_max', value: 16 },
{ field: 'memory_dynamic_max', value: 16 },
{ field: 'memory_dynamic_min', value: 16 },
{ field: 'memory_static_min', value: 16 },
],
},
{
label: 'handles a memory decrease',
targetVm: {
memory_static_min: 4,
memory_dynamic_min: 8,
memory_dynamic_max: 16,
memory_static_max: 16,
},
vmRecord: {
memory_static_min: 2,
memory_dynamic_min: 2,
memory_dynamic_max: 2,
memory_static_max: 2,
},
expectedUpdates: [
{ field: 'memory_static_min', value: 2 },
{ field: 'memory_dynamic_min', value: 2 },
{ field: 'memory_dynamic_max', value: 2 },
{ field: 'memory_static_max', value: 2 },
],
},
{
label: 'handles a more complex case',
targetVm: {
memory_static_min: 1,
memory_dynamic_min: 2,
memory_dynamic_max: 8,
memory_static_max: 16,
},
vmRecord: {
memory_static_min: 4,
memory_dynamic_min: 4,
memory_dynamic_max: 4,
memory_static_max: 4,
},
expectedUpdates: [
{ field: 'memory_dynamic_min', value: 4 },
{ field: 'memory_static_min', value: 4 },
{ field: 'memory_dynamic_max', value: 4 },
{ field: 'memory_static_max', value: 4 },
],
},
{
label: 'avoids updating identical value',
targetVm: {
memory_static_min: 1,
memory_dynamic_min: 2,
memory_dynamic_max: 4,
memory_static_max: 8,
},
vmRecord: {
memory_static_min: 1,
memory_dynamic_min: 2,
memory_dynamic_max: 4,
memory_static_max: 8,
},
expectedUpdates: [],
},
]
describe('updateMemoryFields() should update in the right order', () => {
for (const test of tests) {
it(test.label, async () => {
const mock = mockXapiVm(test.targetVm)
test.targetVm.$ref = MOCK_REF
await updateMemoryFields(mock, test.targetVm, test.vmRecord)
assert.equal(mock.updates.length, test.expectedUpdates.length)
for (let i = 0; i < mock.updates.length; i++) {
assert.equal(mock.updates[i].value, test.expectedUpdates[i].value)
assert.equal(mock.updates[i].field, test.expectedUpdates[i].field)
}
})
}
})

View File

@@ -9,7 +9,9 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
import { FullXapi } from './_vmRunners/FullXapi.mjs'
import { Throttle } from '@vates/generator-toolbox'
import { asyncEach } from '@vates/async-each'
import createStreamThrottle from './_createStreamThrottle.mjs'
import { selectSynchronizedSnapshotVms } from './_selectSynchronizedSnapshotVms.mjs'
const noop = Function.prototype
@@ -31,7 +33,9 @@ const DEFAULT_XAPI_VM_SETTINGS = {
nRetriesVmBackupFailures: 0,
offlineBackup: false,
offlineSnapshot: false,
snapshotConcurrency: 2,
snapshotRetention: 0,
synchronizedSnapshot: false,
timeout: 0,
useNbd: false,
unconditionalSnapshot: false,
@@ -47,6 +51,16 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
return baseSettings
}
_getVmBackup(jobMode, opts) {
if (jobMode === 'delta') {
return new IncrementalXapi(opts)
} else if (jobMode === 'full') {
return new FullXapi(opts)
}
throw new Error(`Job mode ${jobMode} not implemented`)
}
async run() {
const job = this._job
@@ -95,7 +109,81 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
const allSettings = this._job.settings
const baseSettings = this._baseSettings
const queue = new Set(vmIds)
const preTakenTimestampByVmId = {}
const failedSnapshotByVmId = {}
if (settings.synchronizedSnapshot) {
await Task.run({ properties: { name: 'snapshot VMs' } }, async () => {
await Disposable.use(
Disposable.all(vmIds.map(vmId => this._getRecord('VM', vmId).catch(noop))),
async vms => {
// remove vms that failed (already handled)
vms = vms.filter(_ => _ !== undefined)
const batchIds = selectSynchronizedSnapshotVms(settings.synchronizedSnapshot, vms)
await asyncEach(
[...vms].filter(vm => batchIds.has(vm.uuid)),
async vm => {
const vmSettings = { ...settings, ...allSettings[vm.uuid] }
const opts = {
baseSettings,
config,
getSnapshotNameLabel,
healthCheckSr,
job,
remoteAdapters,
schedule,
settings: vmSettings,
srs,
throttleGenerator,
throttleStream,
vm,
}
let vmBackup
try {
vmBackup = this._getVmBackup(job.mode, opts)
if (
!vmBackup._settings.offlineBackup &&
!vmBackup._settings.offlineSnapshot &&
(await vmBackup._mustDoSnapshot())
) {
await vmBackup._prepareAndSnapshot()
preTakenTimestampByVmId[vm.uuid] = vmBackup.timestamp
}
} catch (error) {
failedSnapshotByVmId[vm.uuid] = error
if (vmBackup !== undefined) {
try {
await vmBackup._fetchJobSnapshots()
await vmBackup._removeUnusedSnapshots()
await vmBackup._cleanMetadata()
} catch (cleanupError) {
// Best effort cleanup
}
}
}
},
{
concurrency: settings.snapshotConcurrency,
}
)
}
)
})
}
const snapshotedVmIds = new Set(vmIds.filter(id => !(id in failedSnapshotByVmId)))
Object.entries(failedSnapshotByVmId).forEach(([vmId, error]) => {
Task.run(
{
properties: { id: vmId, name: 'backup VM', type: 'VM' },
},
() => Promise.reject(error)
).catch(noop)
})
const queue = new Set(snapshotedVmIds)
const taskByVmId = {}
const nTriesByVmId = {}
@@ -155,21 +243,16 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
schedule,
settings: vmSettings,
srs,
// when set, the batch phase already snapshotted this VM; the
// runner re-finds that snapshot by its metadata (see _snapshot)
synchronizedSnapshotTimestamp: preTakenTimestampByVmId[vmUuid],
throttleGenerator,
throttleStream,
vm,
}
let vmBackup
if (job.mode === 'delta') {
vmBackup = new IncrementalXapi(opts)
} else {
if (job.mode === 'full') {
vmBackup = new FullXapi(opts)
} else {
throw new Error(`Job mode ${job.mode} not implemented`)
}
}
const vmBackup = this._getVmBackup(job.mode, opts)
return vmBackup.run().catch(error => {
taskError = error
})

View File

@@ -1,13 +1,10 @@
import { asyncMap } from '@xen-orchestra/async-map'
import { createLogger } from '@xen-orchestra/log'
import { Task } from '@vates/task'
import { DIR_XO_POOL_METADATA_BACKUPS } from '../RemoteAdapter.mjs'
import { forkStreamUnpipe } from './_forkStreamUnpipe.mjs'
import { formatFilenameDate } from '../_filenameDate.mjs'
const { debug } = createLogger('xo:backups:PoolMetadataBackup')
export const PATH_DB_DUMP = '/pool/xmldbdump'
export class PoolMetadataBackup {
@@ -34,22 +31,7 @@ export class PoolMetadataBackup {
const poolDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${schedule.id}/${pool.$id}`
const dir = `${poolDir}/${formatFilenameDate(timestamp)}`
const tasks = Object.values(pool.$xapi.objects.indexes.type.task ?? {})
const pendingTasks = tasks.filter(task => task.status === 'pending')
debug('pool tasks before export', {
total: tasks.length,
pending: pendingTasks.length,
oldestPendingCreated: pendingTasks.map(task => task.created).sort()[0],
})
const stream = (await this._exportPoolMetadata()).body
const startedAt = Date.now()
let bytes = 0
stream.once('data', () => debug('first byte received', { afterMs: Date.now() - startedAt }))
stream.on('data', chunk => (bytes += chunk.length))
stream.once('end', () => debug('stream ended', { bytes, durationMs: Date.now() - startedAt }))
stream.once('error', error => debug('stream errored', { error, bytes, durationMs: Date.now() - startedAt }))
const fileName = `${dir}/data`
const metadata = JSON.stringify(

View File

@@ -0,0 +1,19 @@
import { Task } from '@vates/task'
export function selectSynchronizedSnapshotVms(synchronizedSnapshot, vms) {
if (synchronizedSnapshot === false) {
return new Set()
}
let matches
if (typeof synchronizedSnapshot === 'string') {
matches = vms.filter(vm => vm.tags.includes(synchronizedSnapshot))
} else if (synchronizedSnapshot === true) {
matches = vms
} else {
Task.warning('unsupported synchronizedSnapshot option, expected a boolean or a tag name', { synchronizedSnapshot })
return new Set()
}
return new Set(matches.map(vm => vm.uuid))
}

View File

@@ -0,0 +1,73 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { selectSynchronizedSnapshotVms } from './_selectSynchronizedSnapshotVms.mjs'
// Helper mirroring the minimal VM record shape the selector needs.
const vm = (uuid, tags = []) => ({ uuid, tags })
// `selectSynchronizedSnapshotVms(synchronizedSnapshot, vms)` must return a Set
// of the UUIDs that should be snapshotted together in the batch phase.
// - false -> feature off, no VM is batched
// - true -> every VM is batched, regardless of tags
// - '<tagName>' -> only VMs carrying that exact tag are batched
const tests = [
{
label: 'false disables batching',
setting: false,
vms: [vm('a', ['prod']), vm('b')],
expected: [],
},
{
label: 'true batches every VM regardless of tags',
setting: true,
vms: [vm('a', ['prod']), vm('b')],
expected: ['a', 'b'],
},
{
label: 'tag batches only the VMs carrying it',
setting: 'prod',
vms: [vm('a', ['prod']), vm('b', ['dev']), vm('c', ['prod', 'other'])],
expected: ['a', 'c'],
},
{
label: 'tag carried by no VM yields an empty batch',
setting: 'prod',
vms: [vm('a', ['dev']), vm('b')],
expected: [],
},
{
label: 'tag carried by every VM batches all of them',
setting: 'prod',
vms: [vm('a', ['prod']), vm('b', ['prod'])],
expected: ['a', 'b'],
},
{
label: 'empty VM list yields an empty batch',
setting: true,
vms: [],
expected: [],
},
{
label: 'empty-string tag matches no VM when none carries an empty tag',
setting: '',
vms: [vm('a', ['prod'])],
expected: [],
},
{
label: 'non boolean or string yiels an empty batch',
setting: 1,
vms: [vm('a', ['prod'])],
expected: [],
},
]
describe('selectSynchronizedSnapshotVms()', () => {
for (const { label, setting, vms, expected } of tests) {
it(label, () => {
const result = selectSynchronizedSnapshotVms(setting, vms)
assert.ok(result instanceof Set, 'result must be a Set')
assert.deepEqual([...result].sort(), [...expected].sort())
})
}
})

View File

@@ -80,14 +80,14 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
const sizeContainer = watchStreamSize(stream)
const timestamp = Date.now()
const transferStart = Date.now()
await this._callWriters(
writer =>
writer.run({
maxStreamLength,
sizeContainer,
stream: forkStreamUnpipe(stream),
timestamp,
timestamp: this.timestamp,
vm,
vmSnapshot: exportedVm,
}),
@@ -95,8 +95,7 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
)
const { size } = sizeContainer
const end = Date.now()
const duration = end - timestamp
const duration = Date.now() - transferStart
debug('transfer complete', {
duration,
speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,

View File

@@ -13,7 +13,6 @@ import {
DELTA_CHAIN_LENGTH,
EXPORTED_SUCCESSFULLY,
setVmDeltaChainLength,
markExportSuccessfull,
} from '../../_otherConfig.mjs'
import { ThrottledDisk, SynchronizedDisk } from '@xen-orchestra/disk-transform'
import { AggregatedIncrementalRemoteWriter } from '../_writers/AggregatedIncrementalRemoteWriter.mjs'
@@ -68,7 +67,6 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
// @todo : reimplement throttle
const timestamp = Date.now()
await this._callWriters(
writer =>
writer.transfer({
@@ -78,7 +76,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
// clean; the tip's flag lets the next run skip re-probing
includeNonNbdQcow2Fix: true,
isVhdDifferencing,
timestamp,
timestamp: this.timestamp,
vm,
vmSnapshot: exportedVm,
}),
@@ -91,11 +89,6 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, (this._deltaChainLength ?? 0) + 1)
}
// not the case if offlineBackup
if (exportedVm.is_a_snapshot) {
await markExportSuccessfull(this._xapi, exportedVm.$ref)
}
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
}

View File

@@ -7,6 +7,7 @@ import { asyncEach } from '@vates/async-each'
import { decorateMethodsWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { Task } from '@vates/task'
import { formatDateTime } from '@xen-orchestra/xapi'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { Abstract } from './_Abstract.mjs'
@@ -16,9 +17,11 @@ import {
JOB_ID,
SCHEDULE_ID,
VM_UUID,
EXPORTED_SUCCESSFULLY,
resetVmOtherConfig,
setVmOtherConfig,
setVmSnapshotContentKeys,
markExportSuccessfull,
} from '../../_otherConfig.mjs'
const { warn, info } = createLogger('xo:backups:AbstractXapi')
@@ -36,6 +39,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
schedule,
settings,
srs,
synchronizedSnapshotTimestamp,
throttleGenerator,
throttleStream,
vm,
@@ -63,9 +67,9 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
// VM (snapshot) that is really exported
this._exportedVm = undefined
this._synchronizedSnapshotTimestamp = synchronizedSnapshotTimestamp
this._vm = vm
this._baseVdis = undefined
this._getSnapshotNameLabel = getSnapshotNameLabel
this._isIncremental = job.mode === 'delta'
this._healthCheckSr = healthCheckSr
@@ -180,6 +184,27 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
}
async _snapshot() {
if (this._synchronizedSnapshotTimestamp !== undefined) {
const datetime = formatDateTime(this._synchronizedSnapshotTimestamp)
const candidates = this._vm.$snapshots.filter(
snapshot =>
snapshot?.other_config[JOB_ID] === this._jobId &&
snapshot?.other_config[DATETIME] === datetime &&
!snapshot.other_config[EXPORTED_SUCCESSFULLY]
)
if (candidates.length === 1) {
this._exportedVm = candidates[0]
this.timestamp = this._synchronizedSnapshotTimestamp
return
}
warn('expected exactly one synchronized snapshot to reuse, taking a fresh one', {
vm: this._vm.uuid,
datetime,
count: candidates.length,
})
}
const vm = this._vm
const xapi = this._xapi
@@ -396,7 +421,20 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
await xapi.barrier()
// ensure cached object are up to date
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
const disklessVmSnapshots = this._disklessJobSnapshotVms.map(vm => xapi.getObject(vm.$ref))
let disklessVmSnapshots = this._disklessJobSnapshotVms.map(vm => xapi.getObject(vm.$ref))
// The synchronized snapshot for this run is taken up-front by the batch
// phase but transferred later. Until it has been exported, hide it from
// retention so it is neither reclaimed nor allowed to steal the delta
// base's "most recent" protection. This makes the pre-transfer pass behave
// like a normal run, where the snapshot does not exist yet.
if (this._synchronizedSnapshotTimestamp !== undefined) {
const datetime = formatDateTime(this._synchronizedSnapshotTimestamp)
const isInFlightSyncSnapshot = ({ other_config }) =>
other_config[DATETIME] === datetime && !other_config[EXPORTED_SUCCESSFULLY]
this._jobSnapshotVdis = this._jobSnapshotVdis.filter(vdi => !isInFlightSyncSnapshot(vdi))
disklessVmSnapshots = disklessVmSnapshots.filter(vm => !isInFlightSyncSnapshot(vm))
}
// get the datetime of the most recent snapshot across both VDI and diskless VM snapshots
const lastSnapshotDateTime = [...this._jobSnapshotVdis, ...disklessVmSnapshots]
@@ -464,6 +502,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
vm = vdiVm
}
}
if (vm?.$ref !== undefined) {
return xapi.VM_destroy(vm.$ref)
} else {
@@ -493,6 +532,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
return
}
await xapi.VM_destroy(snapshotPerDatetime[datetime])
})
})
@@ -551,6 +591,11 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
throw new Error('Not implemented')
}
async _prepareAndSnapshot() {
await this._cleanMetadata()
await this._snapshot()
}
async run($defer) {
const settings = this._settings
assert(
@@ -614,6 +659,10 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
if (this._writers.size !== 0) {
await this._copy()
}
// not the case if offlineBackup
if (this._exportedVm.is_a_snapshot) {
await markExportSuccessfull(this._xapi, this._exportedVm.$ref)
}
} finally {
if (startAfter) {
ignoreErrors.call(vm.$callAsync('start', false, false))

View File

@@ -0,0 +1,327 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { formatDateTime } from '@xen-orchestra/xapi'
import { AbstractXapi } from './_AbstractXapi.mjs'
import { DATETIME, EXPORTED_SUCCESSFULLY, JOB_ID, SCHEDULE_ID } from '../../_otherConfig.mjs'
// Build an AbstractXapi instance without running its heavy constructor, then
// assign only the fields the method under test reads.
const makeRunner = props => Object.assign(Object.create(AbstractXapi.prototype), props)
describe('_snapshot() synchronized-snapshot reuse guard', () => {
it('reuses the batch snapshot, found by metadata, without snapshotting again', async () => {
let mustDoSnapshotCalls = 0
const timestamp = 1717200000000
// the snapshot taken by the batch phase, identified by its other_config
const batchSnapshot = {
$ref: 'batch-snapshot-ref',
other_config: {
[JOB_ID]: 'job-1',
[SCHEDULE_ID]: 'schedule-1',
[DATETIME]: formatDateTime(timestamp),
},
}
const runner = makeRunner({
_synchronizedSnapshotTimestamp: timestamp,
_jobId: 'job-1',
scheduleId: 'schedule-1',
_vm: { uuid: 'vm-uuid', $snapshots: [batchSnapshot] },
_xapi: { barrier: async () => {} },
// if the metadata lookup fails, _snapshot() falls through to here
_mustDoSnapshot: async () => {
mustDoSnapshotCalls++
return false
},
})
await runner._snapshot()
assert.equal(mustDoSnapshotCalls, 0, '_mustDoSnapshot() should not be called when the batch snapshot is found')
assert.equal(runner._exportedVm, batchSnapshot, '_exportedVm should be the batch snapshot found by metadata')
assert.equal(runner.timestamp, timestamp, 'timestamp should be the synchronized snapshot timestamp')
})
it('takes a fresh snapshot when the batch snapshot can no longer be found', async () => {
let mustDoSnapshotCalls = 0
const vm = { uuid: 'vm-uuid', $snapshots: [] } // the pre-taken snapshot is gone
const runner = makeRunner({
_synchronizedSnapshotTimestamp: 1717200000000,
_jobId: 'job-1',
scheduleId: 'schedule-1',
_vm: vm,
_xapi: { barrier: async () => {} },
_settings: {},
_mustDoSnapshot: async () => {
mustDoSnapshotCalls++
return false
},
})
await runner._snapshot()
assert.equal(
mustDoSnapshotCalls,
1,
'a missing batch snapshot should not be reused: _snapshot() should fall through and take a fresh snapshot'
)
assert.equal(
runner._exportedVm,
vm,
'_exportedVm should fall back to the freshly-snapshotted VM, not stay on a stale/missing batch snapshot'
)
})
it('without a pre-taken snapshot and no snapshot needed, exports the live VM', async () => {
let mustDoSnapshotCalls = 0
const vm = { uuid: 'vm-uuid' }
const runner = makeRunner({
_exportedVm: undefined,
_vm: vm,
_xapi: {},
_settings: {},
_mustDoSnapshot: async () => {
mustDoSnapshotCalls++
return false
},
})
await runner._snapshot()
assert.equal(mustDoSnapshotCalls, 1, '_mustDoSnapshot() should be called once when no snapshot was pre-taken')
assert.equal(runner._exportedVm, vm, '_exportedVm should be set to the live VM when no snapshot is needed')
assert.equal(typeof runner.timestamp, 'number', 'timestamp should be set when exporting the live VM')
})
})
describe('_removeUnusedSnapshots() protects the pre-taken synchronized snapshot', () => {
const OLD_DATETIME = '20240101T00:00:00Z'
const SYNC_TIMESTAMP = 1717200000000
const SYNC_DATETIME = formatDateTime(SYNC_TIMESTAMP)
// Full-mode job with snapshotRetention 0, so retention wants to remove every
// snapshot. `vdi-fresh` is the snapshot taken up-front by the synchronized
// batch phase (identified by SYNC_TIMESTAMP); until it is exported it must be
// hidden from retention.
const makeRemoveRunner = ({ exported }) => {
const destroyed = []
const snapshotVm = ($ref, name_label) => ({
$ref,
name_label,
is_control_domain: false,
$snapshot_of: 'live-vm-ref',
other_config: {},
})
const freshSnapshotVm = snapshotVm('vm-fresh', 'fresh')
const oldSnapshotVm = snapshotVm('vm-old', 'old')
const vdi = ($ref, datetime, snapshotVmRecord, isExported = false) => ({
$ref,
other_config: {
[DATETIME]: datetime,
[SCHEDULE_ID]: 'schedule-1',
...(isExported ? { [EXPORTED_SUCCESSFULLY]: 'true' } : {}),
},
$VBDs: [{ $VM: snapshotVmRecord }],
})
// the old snapshot was exported by a previous run
const oldVdi = vdi('vdi-old', OLD_DATETIME, oldSnapshotVm, true)
const freshVdi = vdi('vdi-fresh', SYNC_DATETIME, freshSnapshotVm, exported)
const registry = { 'vdi-old': oldVdi, 'vdi-fresh': freshVdi }
const runner = makeRunner({
_synchronizedSnapshotTimestamp: SYNC_TIMESTAMP,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [oldVdi, freshVdi],
_disklessJobSnapshotVms: [],
job: { mode: 'full', settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
destroyed.push(ref)
},
VDI_destroy: async ref => {
destroyed.push(ref)
},
},
})
return { runner, destroyed }
}
it('does not destroy the pre-taken snapshot before it has been transferred', async () => {
const { runner, destroyed } = makeRemoveRunner({ exported: false })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, ['vm-old'], 'only the older snapshot should be removed')
})
it('destroys the snapshot once it has been transferred (retention 0, no leak)', async () => {
const { runner, destroyed } = makeRemoveRunner({ exported: true })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed.sort(), ['vm-fresh', 'vm-old'], 'both snapshots should be removed after transfer')
})
})
describe('_removeUnusedSnapshots() in the synchronized batch pre-snapshot state', () => {
it('delta mode: keeps the base snapshot when the fresh one has not been taken yet', async () => {
const destroyed = []
const BASE_DATETIME = '2024-06-01T00:00:00Z'
const baseSnapshotVm = {
$ref: 'vm-base',
name_label: 'base',
is_control_domain: false,
$snapshot_of: 'live-vm-ref',
// exported successfully by the previous run
other_config: { [EXPORTED_SUCCESSFULLY]: 'true' },
}
const baseVdi = {
$ref: 'vdi-base',
other_config: { [DATETIME]: BASE_DATETIME, [SCHEDULE_ID]: 'schedule-1' },
$VBDs: [{ $VM: baseSnapshotVm }],
}
const registry = { 'vdi-base': baseVdi }
const runner = makeRunner({
// batch pre-snapshot state: the synchronized snapshot has not been taken yet
_exportedVm: undefined,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [baseVdi],
_disklessJobSnapshotVms: [],
job: { mode: 'delta', settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
destroyed.push(ref)
},
VDI_destroy: async ref => {
destroyed.push(ref)
},
},
})
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, [], 'the base snapshot must be kept as the delta base for the upcoming transfer')
})
})
describe('_removeUnusedSnapshots() reclaims orphan / CBT snapshot VDIs (no attached VM)', () => {
// Guards the `else` branch: snapshot VDIs that are not attached to any user VM
// (e.g. CBT metadata, orphans) must be reclaimed via VDI_destroy, not VM_destroy.
it('destroys snapshot VDIs directly when they are not attached to any VM', async () => {
const vmDestroyed = []
const vdiDestroyed = []
const orphanVdi = {
$ref: 'vdi-orphan',
other_config: { [DATETIME]: '2024-01-01T00:00:00Z', [SCHEDULE_ID]: 'schedule-1' },
$VBDs: [], // not attached to any VM
}
const registry = { 'vdi-orphan': orphanVdi }
const runner = makeRunner({
_exportedVm: undefined,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [orphanVdi],
_disklessJobSnapshotVms: [],
job: { mode: 'full', settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
vmDestroyed.push(ref)
},
VDI_destroy: async ref => {
vdiDestroyed.push(ref)
},
},
})
await runner._removeUnusedSnapshots()
assert.deepEqual(vdiDestroyed, ['vdi-orphan'], 'the orphan VDI should be reclaimed via VDI_destroy')
assert.deepEqual(vmDestroyed, [], 'no VM should be destroyed for an orphan VDI')
})
})
describe('_removeUnusedSnapshots() diskless VM snapshots', () => {
const OLD_DATETIME = '20240101T00:00:00Z'
const SYNC_TIMESTAMP = 1717200000000
const SYNC_DATETIME = formatDateTime(SYNC_TIMESTAMP)
// A diskless VM's backup snapshots are tracked as VM snapshots (no VDIs to
// anchor them). `dl-fresh` is the snapshot the synchronized batch pre-took,
// identified by SYNC_TIMESTAMP.
const makeDisklessRunner = ({ exported, mode = 'full' }) => {
const destroyed = []
const oldSnap = { $ref: 'dl-old', other_config: { [DATETIME]: OLD_DATETIME, [SCHEDULE_ID]: 'schedule-1' } }
const freshSnap = { $ref: 'dl-fresh', other_config: { [DATETIME]: SYNC_DATETIME, [SCHEDULE_ID]: 'schedule-1' } }
if (exported) freshSnap.other_config[EXPORTED_SUCCESSFULLY] = 'true'
const registry = { 'dl-old': oldSnap, 'dl-fresh': freshSnap }
const runner = makeRunner({
_synchronizedSnapshotTimestamp: SYNC_TIMESTAMP,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [],
_disklessJobSnapshotVms: [oldSnap, freshSnap],
job: { mode, settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
destroyed.push(ref)
},
VDI_destroy: async ref => {
destroyed.push(ref)
},
},
})
return { runner, destroyed }
}
it('does not destroy the pre-taken diskless snapshot before it has been transferred', async () => {
const { runner, destroyed } = makeDisklessRunner({ exported: false })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, ['dl-old'], 'only the older diskless snapshot should be removed')
})
it('destroys the diskless snapshot once it has been transferred (retention 0, no leak)', async () => {
const { runner, destroyed } = makeDisklessRunner({ exported: true })
await runner._removeUnusedSnapshots()
assert.deepEqual(
destroyed.sort(),
['dl-fresh', 'dl-old'],
'both diskless snapshots should be removed after transfer'
)
})
it('keeps the most recent diskless snapshot in delta mode (base for next delta)', async () => {
// Even after transfer (exported), delta mode must retain the latest
// snapshot so the next run can compute its delta against it.
const { runner, destroyed } = makeDisklessRunner({ exported: true, mode: 'delta' })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, ['dl-old'], 'the most recent diskless snapshot should be kept in delta mode')
})
})

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.73.10",
"version": "0.74.0",
"engines": {
"node": ">=14.18"
},
@@ -30,8 +30,8 @@
"@vates/nbd-client": "^3.5.0",
"@vates/parse-duration": "^0.1.1",
"@vates/task": "^0.7.0",
"@vates/types": "^1.30.1",
"@xen-orchestra/async-map": "^0.1.3",
"@vates/types": "^1.30.2",
"@xen-orchestra/async-map": "^0.1.4",
"@xen-orchestra/disk-transform": "^1.3.3",
"@xen-orchestra/fs": "^4.9.3",
"@xen-orchestra/log": "^0.7.2",
@@ -52,11 +52,11 @@
"parse-pairs": "^2.0.0",
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"tar": "^7.5.3",
"tar": "^7.5.19",
"uuid": "^9.0.0",
"value-matcher": "^0.2.0",
"vhd-lib": "^4.16.1",
"xen-api": "^4.8.0",
"xen-api": "^4.9.0",
"yazl": "^2.5.1"
},
"devDependencies": {
@@ -67,7 +67,7 @@
"typescript": "^5.9.3"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^8.10.2"
"@xen-orchestra/xapi": "^8.10.3"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^4.8.0"
"xen-api": "^4.9.0"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/disk-cli",
"version": "2.2.0",
"version": "2.2.1",
"description": "CLI tool to inspect and manage disks in backup repositories",
"type": "module",
"bin": {
@@ -20,7 +20,7 @@
},
"dependencies": {
"@vates/async-each": "^1.0.3",
"@xen-orchestra/backups": "^0.73.10",
"@xen-orchestra/backups": "^0.74.0",
"@xen-orchestra/backup-archive": "^1.1.1",
"@xen-orchestra/disk-transform": "^1.3.3",
"@xen-orchestra/fs": "^4.9.3",

View File

@@ -16,13 +16,13 @@
"xo-immutable-remote": "./dist/protectRemotes.mjs"
},
"license": "AGPL-3.0-or-later",
"version": "3.0.4",
"version": "3.0.5",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@vates/async-each": "^1.0.3",
"@xen-orchestra/backups": "^0.73.10",
"@xen-orchestra/backups": "^0.74.0",
"@xen-orchestra/log": "^0.7.2",
"app-conf": "^3.0.0",
"execa": "^5.0.0",

View File

@@ -1,5 +1,13 @@
# ChangeLog
## **next**
- [Pool/networks] Add the possibility to create new network or bonded network (PR [#10145](https://github.com/vatesfr/xen-orchestra/pull/10145))
- [VM] Add VDIs page with table and side panel (PR [#10269](https://github.com/vatesfr/xen-orchestra/pull/10269))
- [Host/Network] Add ability to rescan physical network interfaces (PIFs) (PR [#10147](https://github.com/vatesfr/xen-orchestra/pull/10147))
- Fix inconsistent spacing in side panel cards (PR [#10279](https://github.com/vatesfr/xen-orchestra/pull/10279))
- Fix missing collapse buttons on pools and hosts in the tree sidebar (added `hasChildren` prop) (PR [#10244](https://github.com/vatesfr/xen-orchestra/pull/10244))
## **0.24.0** (2026-07-30)
- [Host/dashboard] Switch CPU and RAM panels order to match Pool dashboard layout (PR [#10059](https://github.com/vatesfr/xen-orchestra/pull/10059))

View File

@@ -23,18 +23,21 @@
"@fortawesome/vue-fontawesome": "^3.0.8",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@novnc/novnc": "~1.5.0",
"@regle/core": "^1.26.0",
"@regle/rules": "^1.26.0",
"@regle/schemas": "^1.20.2",
"@tsconfig/node18": "^18.2.4",
"@types/d3-time-format": "^4.0.3",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.80",
"@vates/types": "^1.30.1",
"@vates/types": "^1.30.2",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.9.0",
"@vueuse/core": "^14.0.0",
"@vueuse/math": "^14.0.0",
"@vueuse/shared": "^14.0.0",
"@xen-orchestra/web-core": "^0.58.0",
"@xen-orchestra/web-core": "^0.59.0",
"complex-matcher": "^1.1.1",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",

View File

@@ -19,13 +19,13 @@
import AppHeader from '@/components/AppHeader.vue'
import AppLogin from '@/components/AppLogin.vue'
import AppNavigation from '@/components/AppNavigation.vue'
import { useUnreachableHosts } from '@/composables/unreachable-hosts.composable'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import { useUnreachableHosts } from '@/composables/unreachable-hosts.composable.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import VtsOverlayList from '@core/components/overlay/VtsOverlayList.vue'
import VtsTooltipList from '@core/components/tooltip-list/VtsTooltipList.vue'
import { useChartTheme } from '@core/composables/chart-theme.composable'
import { useUiStore } from '@core/stores/ui.store'
import { useChartTheme } from '@core/composables/chart-theme.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { useActiveElement, useMagicKeys, whenever } from '@vueuse/core'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'

View File

@@ -26,11 +26,11 @@
import AccountMenu from '@/components/account-menu/AccountMenu.vue'
import PoolOverrideWarning from '@/components/PoolOverrideWarning.vue'
import XoaButton from '@/components/XoaButton.vue'
import { useNavigationStore } from '@/stores/navigation.store'
import { useNavigationStore } from '@/stores/navigation.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiLogoText from '@core/components/ui/logo-text/UiLogoText.vue'
import { useUiStore } from '@core/stores/ui.store'
import { useUiStore } from '@core/stores/ui.store.ts'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -42,9 +42,9 @@
import FormInput from '@/components/form/FormInput.vue'
import LoginError from '@/components/LoginError.vue'
import PoolOverrideWarning from '@/components/PoolOverrideWarning.vue'
import type { XenApiError } from '@/libs/xen-api/xen-api.types'
import { usePageTitleStore } from '@/stores/page-title.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import type { XenApiError } from '@/libs/xen-api/xen-api.types.ts'
import { usePageTitleStore } from '@/stores/page-title.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts" setup>
import markdown from '@/libs/markdown'
import markdown from '@/libs/markdown.ts'
import { useEventListener } from '@vueuse/core'
import { computed, type Ref, ref } from 'vue'

View File

@@ -10,7 +10,7 @@
<script lang="ts" setup>
import StoryMenu from '@/components/component-story/StoryMenu.vue'
import InfraPoolList from '@/components/infra/InfraPoolList.vue'
import { useNavigationStore } from '@/stores/navigation.store'
import { useNavigationStore } from '@/stores/navigation.store.ts'
import { onClickOutside, tryOnMounted, useTimeoutFn, watchImmediate } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref, watch } from 'vue'

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts" setup>
import { type AcceptedLanguage, highlight } from '@/libs/highlight'
import { type AcceptedLanguage, highlight } from '@/libs/highlight.ts'
import { computed } from 'vue'
const props = withDefaults(

View File

@@ -19,7 +19,7 @@
import UiActionButton from '@/components/ui/UiActionButton.vue'
import UiFilter from '@/components/ui/UiFilter.vue'
import UiFilterGroup from '@/components/ui/UiFilterGroup.vue'
import type { Filters } from '@/types/filter'
import type { Filters } from '@/types/filter.ts'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { useI18n } from 'vue-i18n'

View File

@@ -19,8 +19,8 @@
<script lang="ts" setup>
import UiActionButton from '@/components/ui/UiActionButton.vue'
import { buildComplexMatcherNode } from '@/libs/complex-matcher.utils'
import type { Filter, FilterComparisons, FilterComparisonType, Filters, FilterType, NewFilter } from '@/types/filter'
import { buildComplexMatcherNode } from '@/libs/complex-matcher.utils.ts'
import type { Filter, FilterComparisons, FilterComparisonType, Filters, FilterType, NewFilter } from '@/types/filter.ts'
import VtsSelect from '@core/components/select/VtsSelect.vue'
import UiInput from '@core/components/ui/input/UiInput.vue'
import { useFormSelect } from '@core/packages/form-select'

View File

@@ -22,7 +22,7 @@
import UiActionButton from '@/components/ui/UiActionButton.vue'
import UiFilter from '@/components/ui/UiFilter.vue'
import UiFilterGroup from '@/components/ui/UiFilterGroup.vue'
import type { ActiveSorts, Sorts } from '@/types/sort'
import type { ActiveSorts, Sorts } from '@/types/sort.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { useI18n } from 'vue-i18n'

View File

@@ -14,12 +14,12 @@
</template>
<script lang="ts" setup>
import type { XenApiPatchWithHostRefs } from '@/composables/host-patches.composable'
import type { XenApiPatchWithHostRefs } from '@/composables/host-patches.composable.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import { useTableState } from '@core/composables/table-state.composable'
import { useUiStore } from '@core/stores/ui.store'
import { usePatchColumns } from '@core/tables/column-sets/patch-columns'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { usePatchColumns } from '@core/tables/column-sets/patch-columns.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -11,7 +11,7 @@
</template>
<script lang="ts" setup>
import type { XenApiError } from '@/libs/xen-api/xen-api.types'
import type { XenApiError } from '@/libs/xen-api/xen-api.types.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { useI18n } from 'vue-i18n'

View File

@@ -10,12 +10,12 @@
</template>
<script generic="T extends ObjectType" lang="ts" setup>
import type { ObjectType, ObjectTypeToRecord } from '@/libs/xen-api/xen-api.types'
import type { XapiContext } from '@/stores/xen-api/create-xapi-store-config'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useSrStore } from '@/stores/xen-api/sr.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import type { ObjectType, ObjectTypeToRecord } from '@/libs/xen-api/xen-api.types.ts'
import type { XapiContext } from '@/stores/xen-api/create-xapi-store-config.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { computed, watch } from 'vue'
import { type RouteLocationTyped, type RouteRecordName } from 'vue-router'

View File

@@ -7,7 +7,7 @@
</template>
<script generic="T extends XenApiRecord<ObjectType>, I extends T['uuid']" lang="ts" setup>
import type { ObjectType, XenApiRecord } from '@/libs/xen-api/xen-api.types'
import type { ObjectType, XenApiRecord } from '@/libs/xen-api/xen-api.types.ts'
import ObjectNotFoundView from '@/pages/object-not-found.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { computed } from 'vue'

View File

@@ -27,9 +27,9 @@
</template>
<script lang="ts" setup>
import { useXenApiStore } from '@/stores/xen-api.store'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useSessionStorage } from '@vueuse/core'
import { useI18n } from 'vue-i18n'

View File

@@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
import type { IconName } from '@core/icons'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { computed } from 'vue'

View File

@@ -13,7 +13,7 @@
<script lang="ts" setup>
import TabItem from '@core/components/tab/TabItem.vue'
import { useDisabled } from '@core/composables/disabled.composable'
import { useDisabled } from '@core/composables/disabled.composable.ts'
import type { RouteLocationRaw } from 'vue-router'
const props = defineProps<{

View File

@@ -24,8 +24,8 @@
</template>
<script lang="ts" setup>
import type { PRIMARY_ADDRESS_TYPE } from '@/libs/xen-api/xen-api.enums'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import type { PRIMARY_ADDRESS_TYPE } from '@/libs/xen-api/xen-api.enums.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import UiButton from '@core/components/ui/button/UiButton.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -31,11 +31,11 @@
</template>
<script lang="ts" setup>
import { useXenApiStore } from '@/stores/xen-api.store'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import UiAccountMenuButton from '@core/components/ui/account-menu-button/UiAccountMenuButton.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'

View File

@@ -87,7 +87,7 @@ import {
isSlotParam,
ModelParam,
type Param,
} from '@/libs/story/story-param'
} from '@/libs/story/story-param.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'

View File

@@ -26,7 +26,7 @@
<script lang="ts" setup>
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import type { EventParam } from '@/libs/story/story-param'
import type { EventParam } from '@/libs/story/story-param.ts'
defineProps<{
params: EventParam[]

View File

@@ -83,10 +83,10 @@
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import StoryWidget from '@/components/component-story/StoryWidget.vue'
import useSortedCollection from '@/composables/sorted-collection.composable'
import type { PropParam } from '@/libs/story/story-param'
import useSortedCollection from '@/composables/sorted-collection.composable.ts'
import type { PropParam } from '@/libs/story/story-param.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { useVModel } from '@vueuse/core'
import { toRef } from 'vue'

View File

@@ -37,7 +37,7 @@
<script lang="ts" setup>
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import StoryWidget from '@/components/component-story/StoryWidget.vue'
import type { SettingParam } from '@/libs/story/story-param'
import type { SettingParam } from '@/libs/story/story-param.ts'
import { useVModel } from '@vueuse/core'
const props = defineProps<{

View File

@@ -27,7 +27,7 @@
<script lang="ts" setup>
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import type { SlotParam } from '@/libs/story/story-param'
import type { SlotParam } from '@/libs/story/story-param.ts'
defineProps<{
params: SlotParam[]

View File

@@ -28,7 +28,7 @@ import {
isSelectWidget,
isTextWidget,
type Widget,
} from '@/libs/story/story-widget'
} from '@/libs/story/story-widget.ts'
import { useFormSelect } from '@core/packages/form-select'
import { computed, defineAsyncComponent } from 'vue'

View File

@@ -20,13 +20,13 @@
</template>
<script lang="ts" setup>
import { useContext } from '@/composables/context.composable'
import { ColorContext } from '@/context'
import type { Color } from '@/types'
import { IK_INPUT_ID, IK_INPUT_TYPE } from '@/types/injection-keys'
import { useContext } from '@/composables/context.composable.ts'
import { ColorContext } from '@/context.ts'
import type { Color } from '@/types/index.ts'
import { IK_INPUT_ID, IK_INPUT_TYPE } from '@/types/injection-keys.ts'
import type { IconName } from '@core/icons'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { useDisabled } from '@core/composables/disabled.composable'
import { useDisabled } from '@core/composables/disabled.composable.ts'
import { useVModel } from '@vueuse/core'
import { computed, type HTMLAttributes, inject, ref, useAttrs } from 'vue'

View File

@@ -11,13 +11,13 @@
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -65,10 +65,10 @@ import VtsQuickInfoCard from '@core/components/quick-info-card/VtsQuickInfoCard.
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import useRelativeTime from '@core/composables/relative-time.composable'
import { vTooltip } from '@core/directives/tooltip.directive'
import { formatSizeRaw } from '@core/utils/size.util'
import { parseDateTime } from '@core/utils/time.util'
import useRelativeTime from '@core/composables/relative-time.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { parseDateTime } from '@core/utils/time.util.ts'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -36,7 +36,7 @@ import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { formatSizeRaw } from '@core/utils/size.util'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -20,7 +20,7 @@ import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { useItemCounter } from '@core/composables/item-counter.composable'
import { useItemCounter } from '@core/composables/item-counter.composable.ts'
import { VM_POWER_STATE } from '@vates/types'
import { logicAnd, useSum } from '@vueuse/math'
import { computed } from 'vue'

View File

@@ -1,8 +1,8 @@
<template>
<VtsSidePanel :has-selection="!!pif" @close="emit('close')">
<VtsSidePanel :has-selection="!!pif" class="host-pif-side-panel" @close="emit('close')">
<template v-if="pif" #default>
<!-- PIF -->
<UiCard class="card">
<UiPanelCard>
<VtsCardObjectTitle :id="pif.uuid" :label="isBond ? t('bond') : t('pif')" />
<div class="content">
<!-- NETWORK -->
@@ -75,9 +75,9 @@
</template>
</VtsCardRowKeyValue>
</div>
</UiCard>
</UiPanelCard>
<!-- NETWORK INFORMATION -->
<UiCard class="card">
<UiPanelCard>
<UiCardTitle>{{ t('network-information') }}</UiCardTitle>
<div class="content">
<!-- IP ADDRESSES -->
@@ -180,9 +180,9 @@
</VtsCardRowKeyValue>
</div>
</div>
</UiCard>
</UiPanelCard>
<!-- PROPERTIES -->
<UiCard class="card">
<UiPanelCard>
<UiCardTitle>{{ t('properties') }}</UiCardTitle>
<div class="content">
<!-- MTU -->
@@ -219,16 +219,16 @@
</template>
</VtsCardRowKeyValue>
</div>
</UiCard>
</UiPanelCard>
</template>
</VtsSidePanel>
</template>
<script setup lang="ts">
import type { XenApiPif } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store'
import { usePifStore } from '@/stores/xen-api/pif.store'
import type { XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardObjectTitle from '@core/components/card-object-title/VtsCardObjectTitle.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
@@ -237,10 +237,10 @@ import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import humanFormat from 'human-format'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -317,9 +317,7 @@ const speed = computed(() => {
</script>
<style scoped lang="postcss">
.card {
gap: 1.6rem;
.host-pif-side-panel {
.content {
display: flex;
flex-direction: column;

View File

@@ -2,6 +2,9 @@
<div class="host-pif-table">
<UiTitle>
{{ t('pifs') }}
<template #action>
<slot name="title-actions" />
</template>
</UiTitle>
<div class="container">
<UiQuerySearchBar @search="(value: string) => (searchQuery = value)" />
@@ -22,18 +25,18 @@
</template>
<script lang="ts" setup>
import type { XenApiNetwork, XenApiPif } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import { usePifStore } from '@/stores/xen-api/pif.store'
import type { XenApiNetwork, XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable'
import { useRouteQuery } from '@core/composables/route-query.composable'
import { useTableState } from '@core/composables/table-state.composable'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { icon } from '@core/icons'
import { usePifColumns } from '@core/tables/column-sets/pif-columns'
import { usePifColumns } from '@core/tables/column-sets/pif-columns.ts'
import { logicNot } from '@vueuse/math'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -42,6 +45,10 @@ const { pifs } = defineProps<{
pifs: XenApiPif[]
}>()
defineSlots<{
'title-actions'(): any
}>()
const { isReady, hasError } = usePifStore().subscribe()
const { getByOpaqueRef } = useNetworkStore().subscribe()
const { getPifStatus } = usePifStore().subscribe()

View File

@@ -70,7 +70,7 @@ import UiCard from '@core/components/ui/card/UiCard.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -1,5 +1,11 @@
<template>
<VtsTreeItem v-if="host !== undefined" :expanded="isExpanded" class="infra-host-item" :node-id="`host:${host.uuid}`">
<VtsTreeItem
v-if="host !== undefined"
:expanded="isExpanded"
has-children
class="infra-host-item"
:node-id="`host:${host.uuid}`"
>
<UiTreeItemLabel
:route="{ name: '/host/[uuid]', params: { uuid: host.uuid } }"
icon="object:host"
@@ -40,11 +46,11 @@
<script lang="ts" setup>
import InfraVmItems from '@/components/infra/InfraVmItems.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostTreeActions from '@/modules/host/components/actions/HostTreeActions.vue'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
@@ -52,7 +58,7 @@ import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useToggle } from '@vueuse/shared'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -8,7 +8,7 @@
<script lang="ts" setup>
import InfraHostItem from '@/components/infra/InfraHostItem.vue'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import { useI18n } from 'vue-i18n'

View File

@@ -1,6 +1,6 @@
<template>
<VtsTreeList class="infra-pool-list">
<VtsTreeItem :expanded="isExpanded" :node-id="`pool:${pool?.uuid}`">
<VtsTreeItem :expanded="isExpanded" has-children :node-id="`pool:${pool?.uuid}`">
<VtsTreeItemError v-if="hasError">
{{ t('error-no-data') }}
</VtsTreeItemError>
@@ -41,7 +41,7 @@
import InfraHostItems from '@/components/infra/InfraHostItems.vue'
import InfraVmItems from '@/components/infra/InfraVmItems.vue'
import PoolTreeActions from '@/modules/pool/components/actions/PoolTreeActions.vue'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'

View File

@@ -10,9 +10,9 @@
</template>
<script lang="ts" setup>
import type { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import type { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'

View File

@@ -8,8 +8,8 @@
<script lang="ts" setup>
import InfraVmItem from '@/components/infra/InfraVmItem.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import { computed } from 'vue'

View File

@@ -36,8 +36,8 @@
<script lang="ts" setup>
import CollectionFilterRow from '@/components/CollectionFilterRow.vue'
import UiBadge from '@/components/ui/UiBadge.vue'
import { getFilterIcon } from '@/libs/utils'
import type { Filters, NewFilter } from '@/types/filter'
import { getFilterIcon } from '@/libs/utils.ts'
import type { Filters, NewFilter } from '@/types/filter.ts'
import VtsOverlayButton from '@core/components/overlay/VtsOverlayButton.vue'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'

View File

@@ -15,7 +15,7 @@
</template>
<script lang="ts" setup>
import type { NewSort, Sorts } from '@/types/sort'
import type { NewSort, Sorts } from '@/types/sort.ts'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
import VtsSelect from '@core/components/select/VtsSelect.vue'

View File

@@ -24,8 +24,8 @@
</template>
<script lang="ts" setup>
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
import UiModal from '@core/components/ui/modal/UiModal.vue'
import { computed } from 'vue'

View File

@@ -21,7 +21,7 @@
</template>
<script lang="ts" setup>
import { VM_COMPRESSION_TYPE } from '@/libs/xen-api/xen-api.enums'
import { VM_COMPRESSION_TYPE } from '@/libs/xen-api/xen-api.enums.ts'
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'

View File

@@ -22,7 +22,7 @@
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'

View File

@@ -21,14 +21,14 @@
</template>
<script setup lang="ts">
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types'
import type { NetworkInterface, VmState } from '@/types/new-vm'
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import type { NetworkInterface, VmState } from '@/types/new-vm.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
import { useFormSelect } from '@core/packages/form-select'
import { useNewVmNetworkColumns } from '@core/tables/column-sets/new-vm-network-columns'
import { useNewVmNetworkColumns } from '@core/tables/column-sets/new-vm-network-columns.ts'
import { toRef } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -24,15 +24,15 @@
</template>
<script setup lang="ts">
import type { XenApiSr } from '@/libs/xen-api/xen-api.types'
import type { Vdi, VmState } from '@/types/new-vm'
import type { XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
import type { Vdi, VmState } from '@/types/new-vm.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
import { useFormSelect } from '@core/packages/form-select'
import { useNewVmSrColumns } from '@core/tables/column-sets/new-vm-sr-columns'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell'
import { useNewVmSrColumns } from '@core/tables/column-sets/new-vm-sr-columns.ts'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell.ts'
import { toRef } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -22,14 +22,14 @@
</template>
<script setup lang="ts">
import type { XenApiPif } from '@/libs/xen-api/xen-api.types'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePifStore } from '@/stores/xen-api/pif.store'
import type { XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { computed } from 'vue'

View File

@@ -35,23 +35,23 @@
<script lang="ts" setup>
import ObjectLink from '@/components/ObjectLink.vue'
import type { RawObjectType } from '@/libs/xen-api/xen-api.types'
import { rawTypeToType } from '@/libs/xen-api/xen-api.utils'
import { useAlarmStore } from '@/stores/xen-api/alarm.store'
import type { XenApiAlarm } from '@/types/xen-api'
import type { RawObjectType } from '@/libs/xen-api/xen-api.types.ts'
import { rawTypeToType } from '@/libs/xen-api/xen-api.utils.ts'
import { useAlarmStore } from '@/stores/xen-api/alarm.store.ts'
import type { XenApiAlarm } from '@/types/xen-api.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import { useTableState } from '@core/composables/table-state.composable'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { defineColumn, defineColumns } from '@core/packages/table'
import { useDateColumn } from '@core/tables/column-definitions/date-column'
import { usePercentColumn } from '@core/tables/column-definitions/percent-column'
import { useTextColumn } from '@core/tables/column-definitions/text-column'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell'
import { renderHeadCell } from '@core/tables/helpers/render-head-cell'
import { useDateColumn } from '@core/tables/column-definitions/date-column.ts'
import { usePercentColumn } from '@core/tables/column-definitions/percent-column.ts'
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell.ts'
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
import { logicNot } from '@vueuse/math'
import { h } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -22,10 +22,10 @@
</template>
<script lang="ts" setup>
import { ACTIVE_STATES } from '@/libs/utils'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useVmMetricsStore } from '@/stores/xen-api/vm-metrics.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { ACTIVE_STATES } from '@/libs/utils.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useVmMetricsStore } from '@/stores/xen-api/vm-metrics.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsProgressBar from '@core/components/progress-bar/VtsProgressBar.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'

View File

@@ -14,13 +14,13 @@
<script lang="ts" setup>
import HostsCpuUsage from '@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue'
import VmsCpuUsage from '@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue'
import type { Stat } from '@/composables/fetch-stats.composable'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import type { Stat } from '@/composables/fetch-stats.composable.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import type { XapiHostStatsRaw, XapiVmStatsRaw } from '@vates/types/common'
import { computed, type ComputedRef, inject } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -16,8 +16,8 @@
<script lang="ts" setup>
import HostPatchesTable from '@/components/HostPatchesTable.vue'
import { useHostPatches } from '@/composables/host-patches.composable'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useHostPatches } from '@/composables/host-patches.composable.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { useI18n } from 'vue-i18n'

View File

@@ -16,11 +16,11 @@
</template>
<script lang="ts" setup>
import { formatSize } from '@/libs/utils'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys'
import type { LinearChartData } from '@core/types/chart'
import { formatSize } from '@/libs/utils.ts'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys.ts'
import type { LinearChartData } from '@core/types/chart.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'

View File

@@ -14,13 +14,13 @@
<script lang="ts" setup>
import HostsRamUsage from '@/components/pool/dashboard/ramUsage/HostsRamUsage.vue'
import VmsRamUsage from '@/components/pool/dashboard/ramUsage/VmsRamUsage.vue'
import type { Stat } from '@/composables/fetch-stats.composable'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import type { Stat } from '@/composables/fetch-stats.composable.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import type { XapiHostStatsRaw, XapiVmStatsRaw } from '@vates/types/common'
import type { ComputedRef } from 'vue'
import { computed, inject } from 'vue'

View File

@@ -25,8 +25,8 @@
<script lang="ts" setup>
import PoolDashboardStatusItem from '@/components/pool/dashboard/PoolDashboardStatusItem.vue'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'

View File

@@ -24,7 +24,7 @@
<script lang="ts" setup>
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useSrStore } from '@/stores/xen-api/sr.store'
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
import VtsProgressBarGroup, {
type ProgressBarGroupItem,
} from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'

View File

@@ -16,7 +16,7 @@
<script lang="ts" setup>
import TasksTable from '@/components/tasks/TasksTable.vue'
import { useTaskStore } from '@/stores/xen-api/task.store'
import { useTaskStore } from '@/stores/xen-api/task.store.ts'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'

View File

@@ -12,12 +12,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable'
import { getAvgCpuUsage } from '@/libs/utils'
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { getAvgCpuUsage } from '@/libs/utils.ts'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -18,10 +18,10 @@
</template>
<script lang="ts" setup>
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys'
import type { LinearChartData, ValueFormatter } from '@core/types/chart'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys.ts'
import type { LinearChartData, ValueFormatter } from '@core/types/chart.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'

View File

@@ -14,12 +14,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable'
import { getAvgCpuUsage } from '@/libs/utils'
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { getAvgCpuUsage } from '@/libs/utils.ts'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { IK_VM_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import { IK_VM_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -12,12 +12,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable'
import { parseRamUsage } from '@/libs/utils'
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { parseRamUsage } from '@/libs/utils.ts'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -18,12 +18,12 @@
<script lang="ts" setup>
import SizeStatsSummary from '@/components/ui/SizeStatsSummary.vue'
import { formatSize } from '@/libs/utils'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys'
import type { LinearChartData } from '@core/types/chart'
import { formatSize } from '@/libs/utils.ts'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys.ts'
import type { LinearChartData } from '@core/types/chart.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'

View File

@@ -12,12 +12,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable'
import { parseRamUsage } from '@/libs/utils'
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { parseRamUsage } from '@/libs/utils.ts'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { IK_VM_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import { IK_VM_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -26,15 +26,15 @@
</template>
<script setup lang="ts">
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable'
import { useRouteQuery } from '@core/composables/route-query.composable'
import { useTableState } from '@core/composables/table-state.composable'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { useNetworkColumns } from '@core/tables/column-sets/network-columns.ts'
import { logicNot } from '@vueuse/math'
import { computed, ref } from 'vue'

View File

@@ -1,5 +1,5 @@
<template>
<VtsSidePanel :has-selection="!!network" @close="emit('close')">
<VtsSidePanel :has-selection="!!network" class="pool-network-side-panel" @close="emit('close')">
<template v-if="network" #actions>
<MenuList placement="bottom-end">
<template #trigger="{ open, isOpen }">
@@ -18,7 +18,7 @@
</MenuList>
</template>
<template v-if="network" #default>
<UiCard class="card-container">
<UiPanelCard>
<VtsCardObjectTitle :id="network.uuid" :label="network.name_label" />
<div class="content">
<!-- DESCRIPTION -->
@@ -63,8 +63,8 @@
<template #value>{{ networkDefaultLockingMode }}</template>
</VtsCardRowKeyValue>
</div>
</UiCard>
<UiCard v-if="pifsCount && pifsCount > 0" class="card-container">
</UiPanelCard>
<UiPanelCard v-if="pifsCount && pifsCount > 0">
<div class="typo-body-bold">
{{ t('pifs') }}
<UiCounter :value="pifsCount" variant="primary" size="small" accent="neutral" />
@@ -88,7 +88,7 @@
<PifRow v-for="pif in pifs" :key="pif.uuid" :pif />
</tbody>
</table>
</UiCard>
</UiPanelCard>
</template>
</VtsSidePanel>
</template>
@@ -104,8 +104,8 @@ import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useClipboard } from '@vueuse/core'
import { computed } from 'vue'
@@ -147,11 +147,7 @@ const { copy, copied, isSupported: isClipboardSupported } = useClipboard({ sourc
</script>
<style scoped lang="postcss">
.card-container {
display: flex;
flex-direction: column;
gap: 1.6rem;
.pool-network-side-panel {
.content {
display: flex;
flex-direction: column;

View File

@@ -2,30 +2,15 @@
<div class="pool-networks-table">
<UiTitle>
{{ t('networks') }}
<template #action>
<slot name="title-actions" />
</template>
</UiTitle>
<div class="container">
<div class="table-actions">
<UiQuerySearchBar @search="value => (searchQuery = value)" />
<UiTableActions :title="t('table-actions')">
<UiButton
v-tooltip="copied && t('copied')"
:disabled="!isClipboardSupported"
:left-icon="copied ? 'fa:check-circle' : 'action:copy'"
variant="tertiary"
accent="brand"
size="medium"
@click="selectedNetworkIds.length > 0 && copy()"
>
{{ t('action:copy-info-json') }}
</UiButton>
</UiTableActions>
</div>
<UiQuerySearchBar @search="value => (searchQuery = value)" />
<VtsTable :state :pagination-bindings sticky="right">
<thead>
<tr>
<VtsHeaderCell>
<UiCheckbox v-model="areAllNetworksSelected" accent="brand" />
</VtsHeaderCell>
<HeadCells />
</tr>
</thead>
@@ -35,9 +20,6 @@
:key="network.uuid"
:selected="selectedNetworkId === network.uuid"
>
<UiTableCell>
<UiCheckbox v-model="selectedNetworkIds" :value="network.uuid" accent="brand" />
</UiTableCell>
<BodyCells :item="network" />
</VtsRow>
</tbody>
@@ -46,36 +28,31 @@
</div>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsHeaderCell from '@core/components/table/cells/VtsHeaderCell.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiCheckbox from '@core/components/ui/checkbox/UiCheckbox.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
import UiTableActions from '@core/components/ui/table-actions/UiTableActions.vue'
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import useMultiSelect from '@core/composables/table/multi-select.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useNetworkColumns } from '@core/tables/column-sets/network-columns.ts'
import { useClipboard } from '@vueuse/core'
import { logicNot } from '@vueuse/math'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { networks } = defineProps<{
const { networks, busy, error } = defineProps<{
networks: XenApiNetwork[]
busy?: boolean
error?: boolean
}>()
defineSlots<{
'title-actions'(): any
}>()
const { isReady, hasError } = useNetworkStore().subscribe()
const { records: pifs } = usePifStore().subscribe()
const { getPifCarrier } = usePifMetricsStore().subscribe()
@@ -128,29 +105,9 @@ const filteredNetworks = computed(() => {
const { pageRecords: paginatedNetworks, paginationBindings } = usePagination('networks', filteredNetworks)
const { selected: selectedNetworkIds, areAllSelected: areAllNetworksSelected } = useMultiSelect(
computed(() => networks.map(network => network.uuid)),
computed(() => paginatedNetworks.value.map(network => network.uuid))
)
const selectedNetworksAsJson = computed(() => {
const selectedNetworks = networks.filter(network => selectedNetworkIds.value.includes(network.uuid))
return JSON.stringify(selectedNetworks)
})
const {
copy,
copied,
isSupported: isClipboardSupported,
} = useClipboard({
source: selectedNetworksAsJson,
legacy: true,
})
const state = useTableState({
busy: logicNot(isReady),
error: hasError,
busy: () => busy,
error: () => error,
empty: () =>
networks.length === 0
? t('no-network-detected')
@@ -179,7 +136,7 @@ const { HeadCells, BodyCells } = useNetworkColumns({
})
</script>
<style scoped lang="postcss">
<style lang="postcss" scoped>
.pool-networks-table,
.container {
display: flex;

View File

@@ -19,7 +19,7 @@
</template>
<script setup lang="ts">
import type { XenApiPool } from '@/libs/xen-api/xen-api.types'
import type { XenApiPool } from '@/libs/xen-api/xen-api.types.ts'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
import VtsTabularKeyValueRow from '@core/components/tabular-key-value-row/VtsTabularKeyValueRow.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'

View File

@@ -42,8 +42,8 @@
</template>
<script setup lang="ts">
import type { XenApiPool } from '@/libs/xen-api/xen-api.types'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import type { XenApiPool } from '@/libs/xen-api/xen-api.types.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'

View File

@@ -27,8 +27,8 @@
</template>
<script setup lang="ts">
import type { XenApiNetwork, XenApiPool } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import type { XenApiNetwork, XenApiPool } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
import VtsTabularKeyValueRow from '@core/components/tabular-key-value-row/VtsTabularKeyValueRow.vue'

View File

@@ -58,9 +58,9 @@
</template>
<script setup lang="ts">
import type { XenApiPool, XenApiSr } from '@/libs/xen-api/xen-api.types'
import { useSrStore } from '@/stores/xen-api/sr.store'
import { useVdiStore } from '@/stores/xen-api/vdi.store'
import type { XenApiPool, XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
import { useVdiStore } from '@/stores/xen-api/vdi.store.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'

View File

@@ -17,19 +17,19 @@
</template>
<script lang="ts" setup>
import type { XenApiTask } from '@/libs/xen-api/xen-api.types'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useTaskStore } from '@/stores/xen-api/task.store'
import type { XenApiTask } from '@/libs/xen-api/xen-api.types.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useTaskStore } from '@/stores/xen-api/task.store.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import { useTableState } from '@core/composables/table-state.composable'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { defineColumns } from '@core/packages/table'
import { useDateColumn } from '@core/tables/column-definitions/date-column'
import { useLinkColumn } from '@core/tables/column-definitions/link-column'
import { useProgressBarColumn } from '@core/tables/column-definitions/progress-bar-column'
import { useTextColumn } from '@core/tables/column-definitions/text-column'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell'
import { parseDateTime } from '@core/utils/time.util'
import { useDateColumn } from '@core/tables/column-definitions/date-column.ts'
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
import { useProgressBarColumn } from '@core/tables/column-definitions/progress-bar-column.ts'
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell.ts'
import { parseDateTime } from '@core/utils/time.util.ts'
import { logicNot } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -22,7 +22,7 @@
</template>
<script lang="ts" setup>
import { formatSize, percent } from '@/libs/utils'
import { formatSize, percent } from '@/libs/utils.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -18,7 +18,7 @@
<script lang="ts" setup>
import type { IconName } from '@core/icons'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { useDisabled } from '@core/composables/disabled.composable'
import { useDisabled } from '@core/composables/disabled.composable.ts'
const props = defineProps<{
busy?: boolean

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts" setup>
import { IK_CARD_GROUP_VERTICAL } from '@/types/injection-keys'
import { IK_CARD_GROUP_VERTICAL } from '@/types/injection-keys.ts'
import { inject, provide } from 'vue'
const vertical = inject(IK_CARD_GROUP_VERTICAL, false)

View File

@@ -11,13 +11,13 @@
</template>
<script lang="ts" setup>
import { isVmOperationPending } from '@/libs/vm'
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import { isVmOperationPending } from '@/libs/vm.ts'
import { VM_OPERATION, VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { vTooltip } from '@core/directives/tooltip.directive'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

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