Compare commits
1 Commits
lite/tests
...
lite/ui-ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d0dccf5ba |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,7 +7,6 @@
|
||||
/@vates/*/node_modules/
|
||||
/@xen-orchestra/*/dist/
|
||||
/@xen-orchestra/*/node_modules/
|
||||
/@xen-orchestra/*/.tests-output/
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.2",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"dependencies": {
|
||||
"@vates/multi-key-map": "^0.1.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"ensure-array": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.2.0"
|
||||
"vhd-lib": "^4.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -30,7 +30,6 @@ if (args.length === 0) {
|
||||
|
||||
${name} v${version}
|
||||
`)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.1",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"object-hash": "^2.0.1"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ const PRESETS_RE = /^@babel\/preset-.+$/
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV || 'development'
|
||||
const __PROD__ = NODE_ENV === 'production'
|
||||
const __TEST__ = NODE_ENV === 'test'
|
||||
|
||||
const configs = {
|
||||
'@babel/plugin-proposal-decorators': {
|
||||
@@ -14,7 +15,7 @@ const configs = {
|
||||
proposal: 'minimal',
|
||||
},
|
||||
'@babel/preset-env': {
|
||||
debug: __PROD__,
|
||||
debug: !__TEST__,
|
||||
|
||||
// disabled until https://github.com/babel/babel/issues/8323 is resolved
|
||||
// loose: true,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import getopts from 'getopts'
|
||||
'use strict'
|
||||
|
||||
const { version } = JSON.parse(readFileSync(new URL('package.json', import.meta.url)))
|
||||
const getopts = require('getopts')
|
||||
|
||||
export function composeCommands(commands) {
|
||||
return async function (args, prefix) {
|
||||
const { version } = require('./package.json')
|
||||
|
||||
module.exports = commands =>
|
||||
async function (args, prefix) {
|
||||
const opts = getopts(args, {
|
||||
alias: {
|
||||
help: 'h',
|
||||
@@ -29,6 +30,5 @@ xo-backups v${version}
|
||||
return
|
||||
}
|
||||
|
||||
return (await command.default)(args.slice(1), prefix + ' ' + commandName)
|
||||
return command.main(args.slice(1), prefix + ' ' + commandName)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import fs from 'fs/promises'
|
||||
import { dirname } from 'path'
|
||||
'use strict'
|
||||
|
||||
export * from 'fs/promises'
|
||||
const { dirname } = require('path')
|
||||
|
||||
export const getSize = path =>
|
||||
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
|
||||
module.exports = fs
|
||||
|
||||
fs.getSize = path =>
|
||||
fs.stat(path).then(
|
||||
_ => _.size,
|
||||
error => {
|
||||
@@ -14,7 +16,7 @@ export const getSize = path =>
|
||||
}
|
||||
)
|
||||
|
||||
export async function mktree(path) {
|
||||
fs.mktree = async function mkdirp(path) {
|
||||
try {
|
||||
await fs.mkdir(path)
|
||||
} catch (error) {
|
||||
@@ -24,8 +26,8 @@ export async function mktree(path) {
|
||||
return
|
||||
}
|
||||
if (code === 'ENOENT') {
|
||||
await mktree(dirname(path))
|
||||
return mktree(path)
|
||||
await mkdirp(dirname(path))
|
||||
return mkdirp(path)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -35,7 +37,7 @@ export async function mktree(path) {
|
||||
// - single param for direct use in `Array#map`
|
||||
// - files are prefixed with directory path
|
||||
// - safer: returns empty array if path is missing or not a directory
|
||||
export const readdir2 = path =>
|
||||
fs.readdir2 = path =>
|
||||
fs.readdir(path).then(
|
||||
entries => {
|
||||
entries.forEach((entry, i) => {
|
||||
@@ -57,7 +59,7 @@ export const readdir2 = path =>
|
||||
}
|
||||
)
|
||||
|
||||
export async function symlink2(target, path) {
|
||||
fs.symlink2 = async (target, path) => {
|
||||
try {
|
||||
await fs.symlink(target, path)
|
||||
} catch (error) {
|
||||
40
@xen-orchestra/backups-cli/commands/clean-vms.js
Normal file
40
@xen-orchestra/backups-cli/commands/clean-vms.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const asyncMap = require('lodash/curryRight')(require('@xen-orchestra/async-map').asyncMap)
|
||||
const getopts = require('getopts')
|
||||
const { RemoteAdapter } = require('@xen-orchestra/backups/RemoteAdapter')
|
||||
const { resolve } = require('path')
|
||||
|
||||
const adapter = new RemoteAdapter(require('@xen-orchestra/fs').getHandler({ url: 'file://' }))
|
||||
|
||||
module.exports = async function main(args) {
|
||||
const { _, fix, remove, merge } = getopts(args, {
|
||||
alias: {
|
||||
fix: 'f',
|
||||
remove: 'r',
|
||||
merge: 'm',
|
||||
},
|
||||
boolean: ['fix', 'merge', 'remove'],
|
||||
default: {
|
||||
merge: false,
|
||||
remove: false,
|
||||
},
|
||||
})
|
||||
|
||||
await asyncMap(_, async vmDir => {
|
||||
vmDir = resolve(vmDir)
|
||||
try {
|
||||
await adapter.cleanVm(vmDir, {
|
||||
fixMetadata: fix,
|
||||
remove,
|
||||
merge,
|
||||
logInfo: (...args) => console.log(...args),
|
||||
logWarn: (...args) => console.warn(...args),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('adapter.cleanVm', vmDir, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { asyncMap } from '@xen-orchestra/async-map'
|
||||
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter.js'
|
||||
import { getSyncedHandler } from '@xen-orchestra/fs'
|
||||
import getopts from 'getopts'
|
||||
import { basename, dirname } from 'path'
|
||||
import Disposable from 'promise-toolbox/Disposable'
|
||||
import { pathToFileURL } from 'url'
|
||||
|
||||
export default async function cleanVms(args) {
|
||||
const { _, fix, remove, merge } = getopts(args, {
|
||||
alias: {
|
||||
fix: 'f',
|
||||
remove: 'r',
|
||||
merge: 'm',
|
||||
},
|
||||
boolean: ['fix', 'merge', 'remove'],
|
||||
default: {
|
||||
merge: false,
|
||||
remove: false,
|
||||
},
|
||||
})
|
||||
|
||||
await asyncMap(_, vmDir =>
|
||||
Disposable.use(getSyncedHandler({ url: pathToFileURL(dirname(vmDir)).href }), async handler => {
|
||||
try {
|
||||
await new RemoteAdapter(handler).cleanVm(basename(vmDir), {
|
||||
fixMetadata: fix,
|
||||
remove,
|
||||
merge,
|
||||
logInfo: (...args) => console.log(...args),
|
||||
logWarn: (...args) => console.warn(...args),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('adapter.cleanVm', vmDir, error)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { mktree, readdir2, readFile, symlink2 } from '../_fs.mjs'
|
||||
import { asyncMap } from '@xen-orchestra/async-map'
|
||||
import filenamify from 'filenamify'
|
||||
import get from 'lodash/get.js'
|
||||
import { dirname, join, relative } from 'path'
|
||||
'use strict'
|
||||
|
||||
export default async function createSymlinkIndex([backupDir, fieldPath]) {
|
||||
const filenamify = require('filenamify')
|
||||
const get = require('lodash/get')
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { dirname, join, relative } = require('path')
|
||||
|
||||
const { mktree, readdir2, readFile, symlink2 } = require('../_fs')
|
||||
|
||||
module.exports = async function createSymlinkIndex([backupDir, fieldPath]) {
|
||||
const indexDir = join(backupDir, 'indexes', filenamify(fieldPath))
|
||||
await mktree(indexDir)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { readdir2, readFile, getSize } from '../_fs.mjs'
|
||||
import { asyncMap } from '@xen-orchestra/async-map'
|
||||
import { createHash } from 'crypto'
|
||||
import groupBy from 'lodash/groupBy.js'
|
||||
import { dirname, resolve } from 'path'
|
||||
'use strict'
|
||||
|
||||
const groupBy = require('lodash/groupBy')
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { createHash } = require('crypto')
|
||||
const { dirname, resolve } = require('path')
|
||||
|
||||
const { readdir2, readFile, getSize } = require('../_fs')
|
||||
|
||||
const sha512 = str => createHash('sha512').update(str).digest('hex')
|
||||
const sum = values => values.reduce((a, b) => a + b)
|
||||
|
||||
export default async function info(vmDirs) {
|
||||
module.exports = async function info(vmDirs) {
|
||||
const jsonFiles = (
|
||||
await asyncMap(vmDirs, async vmDir => (await readdir2(vmDir)).filter(_ => _.endsWith('.json')))
|
||||
).flat()
|
||||
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
import { composeCommands } from './_composeCommands.mjs'
|
||||
|
||||
const importDefault = async path => (await import(path)).default
|
||||
'use strict'
|
||||
|
||||
composeCommands({
|
||||
require('./_composeCommands')({
|
||||
'clean-vms': {
|
||||
get default() {
|
||||
return importDefault('./commands/clean-vms.mjs')
|
||||
get main() {
|
||||
return require('./commands/clean-vms')
|
||||
},
|
||||
usage: `[--fix] [--merge] [--remove] xo-vm-backups/*
|
||||
|
||||
@@ -19,14 +18,14 @@ composeCommands({
|
||||
`,
|
||||
},
|
||||
'create-symlink-index': {
|
||||
get default() {
|
||||
return importDefault('./commands/create-symlink-index.mjs')
|
||||
get main() {
|
||||
return require('./commands/create-symlink-index')
|
||||
},
|
||||
usage: 'xo-vm-backups <field path>',
|
||||
},
|
||||
info: {
|
||||
get default() {
|
||||
return importDefault('./commands/info.mjs')
|
||||
get main() {
|
||||
return require('./commands/info')
|
||||
},
|
||||
usage: 'xo-vm-backups/*',
|
||||
},
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"private": false,
|
||||
"bin": {
|
||||
"xo-backups": "index.mjs"
|
||||
"xo-backups": "index.js"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.2",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"@xen-orchestra/backups": "^0.29.0",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=7.10.1"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups-cli",
|
||||
"name": "@xen-orchestra/backups-cli",
|
||||
@@ -27,7 +27,7 @@
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "0.7.8",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
|
||||
@@ -508,7 +508,7 @@ class RemoteAdapter {
|
||||
return `${BACKUP_DIR}/${vmUuid}/cache.json.gz`
|
||||
}
|
||||
|
||||
async _readCache(path) {
|
||||
async #readCache(path) {
|
||||
try {
|
||||
return JSON.parse(await fromCallback(zlib.gunzip, await this.handler.readFile(path)))
|
||||
} catch (error) {
|
||||
@@ -521,15 +521,15 @@ class RemoteAdapter {
|
||||
_updateCache = synchronized.withKey()(this._updateCache)
|
||||
// eslint-disable-next-line no-dupe-class-members
|
||||
async _updateCache(path, fn) {
|
||||
const cache = await this._readCache(path)
|
||||
const cache = await this.#readCache(path)
|
||||
if (cache !== undefined) {
|
||||
fn(cache)
|
||||
|
||||
await this._writeCache(path, cache)
|
||||
await this.#writeCache(path, cache)
|
||||
}
|
||||
}
|
||||
|
||||
async _writeCache(path, data) {
|
||||
async #writeCache(path, data) {
|
||||
try {
|
||||
await this.handler.writeFile(path, await fromCallback(zlib.gzip, JSON.stringify(data)), { flags: 'w' })
|
||||
} catch (error) {
|
||||
@@ -537,6 +537,10 @@ class RemoteAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateVmBackupListCache(vmUuid) {
|
||||
await this.handler.unlink(this.#getVmBackupsCache(vmUuid))
|
||||
}
|
||||
|
||||
async #getCachabledDataListVmBackups(dir) {
|
||||
debug('generating cache', { path: dir })
|
||||
|
||||
@@ -577,7 +581,7 @@ class RemoteAdapter {
|
||||
async _readCacheListVmBackups(vmUuid) {
|
||||
const path = this.#getVmBackupsCache(vmUuid)
|
||||
|
||||
const cache = await this._readCache(path)
|
||||
const cache = await this.#readCache(path)
|
||||
if (cache !== undefined) {
|
||||
debug('found VM backups cache, using it', { path })
|
||||
return cache
|
||||
@@ -590,7 +594,7 @@ class RemoteAdapter {
|
||||
}
|
||||
|
||||
// detached async action, will not reject
|
||||
this._writeCache(path, backups)
|
||||
this.#writeCache(path, backups)
|
||||
|
||||
return backups
|
||||
}
|
||||
|
||||
@@ -311,6 +311,7 @@ exports.cleanVm = async function cleanVm(
|
||||
}
|
||||
|
||||
const jsons = new Set()
|
||||
let mustInvalidateCache = false
|
||||
const xvas = new Set()
|
||||
const xvaSums = []
|
||||
const entries = await handler.list(vmDir, {
|
||||
@@ -326,20 +327,6 @@ exports.cleanVm = async function cleanVm(
|
||||
}
|
||||
})
|
||||
|
||||
const cachePath = vmDir + '/cache.json.gz'
|
||||
|
||||
let mustRegenerateCache
|
||||
{
|
||||
const cache = await this._readCache(cachePath)
|
||||
const actual = cache === undefined ? 0 : Object.keys(cache).length
|
||||
const expected = jsons.size
|
||||
|
||||
mustRegenerateCache = actual !== expected
|
||||
if (mustRegenerateCache) {
|
||||
logWarn('unexpected number of entries in backup cache', { path: cachePath, actual, expected })
|
||||
}
|
||||
}
|
||||
|
||||
await asyncMap(xvas, async path => {
|
||||
// check is not good enough to delete the file, the best we can do is report
|
||||
// it
|
||||
@@ -351,8 +338,6 @@ exports.cleanVm = async function cleanVm(
|
||||
const unusedVhds = new Set(vhds)
|
||||
const unusedXvas = new Set(xvas)
|
||||
|
||||
const backups = new Map()
|
||||
|
||||
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
||||
// reference a missing XVA/VHD
|
||||
await asyncMap(jsons, async json => {
|
||||
@@ -365,16 +350,19 @@ exports.cleanVm = async function cleanVm(
|
||||
return
|
||||
}
|
||||
|
||||
let isBackupComplete
|
||||
|
||||
const { mode } = metadata
|
||||
if (mode === 'full') {
|
||||
const linkedXva = resolve('/', vmDir, metadata.xva)
|
||||
isBackupComplete = xvas.has(linkedXva)
|
||||
if (isBackupComplete) {
|
||||
if (xvas.has(linkedXva)) {
|
||||
unusedXvas.delete(linkedXva)
|
||||
} else {
|
||||
logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
|
||||
if (remove) {
|
||||
logInfo('deleting incomplete backup', { path: json })
|
||||
jsons.delete(json)
|
||||
mustInvalidateCache = true
|
||||
await handler.unlink(json)
|
||||
}
|
||||
}
|
||||
} else if (mode === 'delta') {
|
||||
const linkedVhds = (() => {
|
||||
@@ -383,28 +371,22 @@ exports.cleanVm = async function cleanVm(
|
||||
})()
|
||||
|
||||
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
||||
isBackupComplete = missingVhds.length === 0
|
||||
|
||||
// FIXME: find better approach by keeping as much of the backup as
|
||||
// possible (existing disks) even if one disk is missing
|
||||
if (isBackupComplete) {
|
||||
if (missingVhds.length === 0) {
|
||||
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
||||
linkedVhds.forEach(path => {
|
||||
vhdsToJSons[path] = json
|
||||
})
|
||||
} else {
|
||||
logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
|
||||
}
|
||||
}
|
||||
|
||||
if (isBackupComplete) {
|
||||
backups.set(json, metadata)
|
||||
} else {
|
||||
jsons.delete(json)
|
||||
if (remove) {
|
||||
logInfo('deleting incomplete backup', { backup: json })
|
||||
mustRegenerateCache = true
|
||||
await handler.unlink(json)
|
||||
if (remove) {
|
||||
logInfo('deleting incomplete backup', { path: json })
|
||||
mustInvalidateCache = true
|
||||
jsons.delete(json)
|
||||
await handler.unlink(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -514,7 +496,7 @@ exports.cleanVm = async function cleanVm(
|
||||
// check for the other that the size is the same as the real file size
|
||||
|
||||
await asyncMap(jsons, async metadataPath => {
|
||||
const metadata = backups.get(metadataPath)
|
||||
const metadata = JSON.parse(await handler.readFile(metadataPath))
|
||||
|
||||
let fileSystemSize
|
||||
const merged = metadataWithMergedVhd[metadataPath] !== undefined
|
||||
@@ -556,7 +538,6 @@ exports.cleanVm = async function cleanVm(
|
||||
// systematically update size after a merge
|
||||
if ((merged || fixMetadata) && size !== fileSystemSize) {
|
||||
metadata.size = fileSystemSize
|
||||
mustRegenerateCache = true
|
||||
try {
|
||||
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
||||
} catch (error) {
|
||||
@@ -565,16 +546,9 @@ exports.cleanVm = async function cleanVm(
|
||||
}
|
||||
})
|
||||
|
||||
if (mustRegenerateCache) {
|
||||
const cache = {}
|
||||
for (const [path, content] of backups.entries()) {
|
||||
cache[path] = {
|
||||
_filename: path,
|
||||
id: path,
|
||||
...content,
|
||||
}
|
||||
}
|
||||
await this._writeCache(cachePath, cache)
|
||||
// purge cache if a metadata file has been deleted
|
||||
if (mustInvalidateCache) {
|
||||
await handler.unlink(vmDir + '/cache.json.gz')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.29.2",
|
||||
"version": "0.29.0",
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
},
|
||||
@@ -21,13 +21,13 @@
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.3",
|
||||
"@vates/disposable": "^0.1.2",
|
||||
"@vates/fuse-vhd": "^1.0.0",
|
||||
"@vates/nbd-client": "*",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"compare-versions": "^5.0.1",
|
||||
"d3-time-format": "^3.0.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.2.0",
|
||||
"vhd-lib": "^4.1.1",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xen-orchestra/xapi": "^1.5.3"
|
||||
"@xen-orchestra/xapi": "^1.5.2"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
|
||||
@@ -49,6 +49,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
|
||||
const dataBasename = basename + '.xva'
|
||||
const dataFilename = backupDir + '/' + dataBasename
|
||||
|
||||
const metadataFilename = `${backupDir}/${basename}.json`
|
||||
const metadata = {
|
||||
jobId: job.id,
|
||||
mode: job.mode,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "3.3.0",
|
||||
"version": "3.2.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"bind-property-descriptor": "^2.0.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"execa": "^5.0.0",
|
||||
|
||||
@@ -284,25 +284,15 @@ export default class RemoteHandlerAbstract {
|
||||
return this._encryptor.decryptData(data)
|
||||
}
|
||||
|
||||
async #rename(oldPath, newPath, { checksum }, createTree = true) {
|
||||
try {
|
||||
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
|
||||
if (checksum) {
|
||||
p = Promise.all([p, this._rename(checksumFile(oldPath), checksumFile(newPath))])
|
||||
}
|
||||
await p
|
||||
} catch (error) {
|
||||
// ENOENT can be a missing target directory OR a missing source
|
||||
if (error.code === 'ENOENT' && createTree) {
|
||||
await this._mktree(dirname(newPath))
|
||||
return this.#rename(oldPath, newPath, { checksum }, false)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
async rename(oldPath, newPath, { checksum = false } = {}) {
|
||||
oldPath = normalizePath(oldPath)
|
||||
newPath = normalizePath(newPath)
|
||||
|
||||
rename(oldPath, newPath, { checksum = false } = {}) {
|
||||
return this.#rename(normalizePath(oldPath), normalizePath(newPath), { checksum })
|
||||
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
|
||||
if (checksum) {
|
||||
p = Promise.all([p, this._rename(checksumFile(oldPath), checksumFile(newPath))])
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
async copy(oldPath, newPath, { checksum = false } = {}) {
|
||||
|
||||
@@ -228,17 +228,6 @@ handlers.forEach(url => {
|
||||
expect(await handler.list('.')).toEqual(['file2'])
|
||||
expect(await handler.readFile(`file2`)).toEqual(TEST_DATA)
|
||||
})
|
||||
it(`should rename the file and create dest directory`, async () => {
|
||||
await handler.outputFile('file', TEST_DATA)
|
||||
await handler.rename('file', `sub/file2`)
|
||||
|
||||
expect(await handler.list('sub')).toEqual(['file2'])
|
||||
expect(await handler.readFile(`sub/file2`)).toEqual(TEST_DATA)
|
||||
})
|
||||
it(`should fail with enoent if source file is missing`, async () => {
|
||||
const error = await rejectionOf(handler.rename('file', `sub/file2`))
|
||||
expect(error.code).toBe('ENOENT')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#rmdir()', () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ module.exports = {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@limegrass/import-alias/import-alias": [
|
||||
"error",
|
||||
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.app.json") },
|
||||
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.json") },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
// Keeping this file to prevent applying the global monorepo config for now
|
||||
module.exports = {};
|
||||
module.exports = {
|
||||
importOrder: [
|
||||
"^[^/]+$",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^@/components/(.*)$",
|
||||
"^@/composables/(.*)$",
|
||||
"^@/libs/(.*)$",
|
||||
"^@/router/(.*)$",
|
||||
"^@/stores/(.*)$",
|
||||
"^@/views/(.*)$",
|
||||
],
|
||||
importOrderSeparation: false,
|
||||
importOrderSortSpecifiers: true,
|
||||
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
- Invalidate sessionId token after logout (PR [#6480](https://github.com/vatesfr/xen-orchestra/pull/6480))
|
||||
- Settings page (PR [#6418](https://github.com/vatesfr/xen-orchestra/pull/6418))
|
||||
- Uncollapse hosts in the tree by default (PR [#6428](https://github.com/vatesfr/xen-orchestra/pull/6428))
|
||||
- Display RAM usage in pool dashboard (PR [#6419](https://github.com/vatesfr/xen-orchestra/pull/6419))
|
||||
|
||||
## **0.1.0**
|
||||
|
||||
|
||||
@@ -91,21 +91,18 @@ const fontSize = ref("2rem");
|
||||
|
||||
This project is using Font Awesome 6 Free.
|
||||
|
||||
Icons can be displayed with the `UiIcon` component.
|
||||
Here is how to use an icon in your template.
|
||||
|
||||
Passing `undefined` as `icon` prop will disable the component (no need to use an additional `v-if` condition).
|
||||
|
||||
Use the `busy` prop to display a loader icon.
|
||||
Note: `FontAwesomeIcon` is a global component that does not need to be imported.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<UiIcon :icon="faDisplay" />
|
||||
<FontAwesomeIcon :icon="faDisplay" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
</script>
|
||||
```
|
||||
@@ -118,6 +115,8 @@ Here is the equivalent between font weight and style name.
|
||||
| ---------- | ----------- |
|
||||
| Solid | 900 |
|
||||
| Regular | 400 |
|
||||
| Light | 300 |
|
||||
| Thin | 100 |
|
||||
|
||||
### CSS
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["./**/*"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// See here how to get started:
|
||||
// https://playwright.dev/docs/intro
|
||||
test("visits the app root url", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("login-form")).toBeTruthy();
|
||||
});
|
||||
@@ -3,16 +3,12 @@
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
|
||||
"build": "run-p test:type-check build-only",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "GIT_HEAD=$(git rev-parse HEAD) vite build",
|
||||
"deploy": "./scripts/deploy.sh",
|
||||
"test": "run-p test:once test:type-check",
|
||||
"test:once": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:type-check": "vue-tsc --noEmit"
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
@@ -22,8 +18,7 @@
|
||||
"@novnc/novnc": "^1.3.0",
|
||||
"@types/d3-time-format": "^4.0.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@vueuse/core": "^9.5.0",
|
||||
"@vueuse/math": "^9.5.0",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"complex-matcher": "^0.7.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
@@ -44,24 +39,19 @@
|
||||
"devDependencies": {
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||
"@limegrass/eslint-plugin-import-alias": "^1.0.5",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@testing-library/vue": "^6.6.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
|
||||
"@types/node": "^16.11.41",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vitest/coverage-c8": "^0.25.3",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"happy-dom": "^7.7.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-nested": "^6.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.4",
|
||||
"vitest": "^0.25.3",
|
||||
"vue-tsc": "^1.0.9"
|
||||
"postcss-nested": "^5.0.6",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^2.9.12",
|
||||
"vue-tsc": "^0.38.1"
|
||||
},
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/lite",
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./e2e",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { outputFolder: ".tests-output/e2e-report" }]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://localhost:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
||||
/* Only on CI systems run the tests headless */
|
||||
headless: !!process.env.CI,
|
||||
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
use: {
|
||||
...devices["Desktop Firefox"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
use: {
|
||||
...devices["Desktop Safari"],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: ".tests-output/e2e-result",
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
/**
|
||||
* Use the dev server by default for faster feedback loop.
|
||||
* Use the preview server on CI for more realistic testing.
|
||||
Playwright will re-use the local server if there is already a dev-server running.
|
||||
*/
|
||||
command: process.env.CI ? "vite preview --port 3000" : "vite dev",
|
||||
port: 5173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -32,9 +32,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
|
||||
import { logicAnd } from "@vueuse/math";
|
||||
import { difference } from "lodash";
|
||||
import { computed, ref, watch, watchEffect } from "vue";
|
||||
import favicon from "@/assets/favicon.svg";
|
||||
@@ -61,28 +58,13 @@ link.href = favicon;
|
||||
|
||||
document.title = "XO Lite";
|
||||
|
||||
if (window.localStorage?.getItem("colorMode") !== "light") {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
|
||||
const xenApiStore = useXenApiStore();
|
||||
const hostStore = useHostStore();
|
||||
useChartTheme();
|
||||
const uiStore = useUiStore();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const activeElement = useActiveElement();
|
||||
const { D } = useMagicKeys();
|
||||
|
||||
const canToggleDarkMode = computed(() => {
|
||||
if (activeElement.value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !["INPUT", "TEXTAREA"].includes(activeElement.value.tagName);
|
||||
});
|
||||
|
||||
whenever(
|
||||
logicAnd(D, canToggleDarkMode),
|
||||
() => (uiStore.colorMode = uiStore.colorMode === "dark" ? "light" : "dark")
|
||||
);
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (xenApiStore.isConnected) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import AccountButton from '@/components/AccountButton.vue'
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="app-login form-container">
|
||||
<form @submit.prevent="handleSubmit" data-testid="login-form">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<img alt="XO Lite" src="../assets/logo-title.svg" />
|
||||
<input v-model="login" name="login" readonly type="text" />
|
||||
<input
|
||||
|
||||
@@ -43,14 +43,14 @@
|
||||
|
||||
<template #buttons>
|
||||
<UiButton transparent @click="addNewFilter">
|
||||
{{ $t("add-or") }}
|
||||
</UiButton>
|
||||
{{ $t("add-or") }}
|
||||
</UiButton>
|
||||
<UiButton :disabled="!isFilterValid" type="submit">
|
||||
{{ $t(editedFilter ? "update" : "add") }}
|
||||
</UiButton>
|
||||
<UiButton outlined @click="handleCancel">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@remove="emit('removeSort', property)"
|
||||
>
|
||||
<span class="property">
|
||||
<FontAwesomeIcon :icon="isAscending ? faCaretUp : faCaretDown" />
|
||||
<UiIcon :icon="isAscending ? faCaretUp : faCaretDown" />
|
||||
{{ property }}
|
||||
</span>
|
||||
</UiFilter>
|
||||
@@ -41,8 +41,8 @@
|
||||
<template #buttons>
|
||||
<UiButton type="submit">{{ $t("add") }}</UiButton>
|
||||
<UiButton outlined @click="handleCancel">
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
{{ $t("cancel") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
</template>
|
||||
@@ -57,10 +57,11 @@ import {
|
||||
faSort,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiActionButton from "@/components/ui/UiActionButton.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiFilter from "@/components/ui/UiFilter.vue";
|
||||
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<th>
|
||||
<div class="content">
|
||||
<span class="label">
|
||||
<FontAwesomeIcon v-if="icon" :icon="icon" />
|
||||
<UiIcon :icon="icon" />
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
icon?: IconDefinition;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<span class="widget">
|
||||
<span v-if="before || $slots.before" class="before">
|
||||
<slot name="before">
|
||||
<FontAwesomeIcon v-if="isIcon(before)" :icon="before" fixed-width />
|
||||
<UiIcon v-if="isIcon(before)" :icon="before" fixed-width />
|
||||
<template v-else>{{ before }}</template>
|
||||
</slot>
|
||||
</span>
|
||||
@@ -17,7 +17,7 @@
|
||||
</span>
|
||||
<span v-if="after || $slots.after" class="after">
|
||||
<slot name="after">
|
||||
<FontAwesomeIcon v-if="isIcon(after)" :icon="after" fixed-width />
|
||||
<UiIcon v-if="isIcon(after)" :icon="after" fixed-width />
|
||||
<template v-else>{{ after }}</template>
|
||||
</slot>
|
||||
</span>
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
before?: IconDefinition | string | object; // "object" added as workaround
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import PowerStateIcon from "@/components/PowerStateIcon.vue";
|
||||
|
||||
describe("PowerStateIcon.vue", () => {
|
||||
it("should render correctly", async () => {
|
||||
const wrapper = mount(PowerStateIcon, {
|
||||
props: {
|
||||
state: "Running",
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.element.classList.contains("state-running")).toBeTruthy();
|
||||
|
||||
await wrapper.setProps({
|
||||
state: "Paused",
|
||||
});
|
||||
|
||||
expect(wrapper.element.classList.contains("state-paused")).toBeTruthy();
|
||||
|
||||
await wrapper.setProps({
|
||||
state: "not-exists",
|
||||
});
|
||||
|
||||
expect(wrapper.element.classList.contains("state-not-exists")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<FontAwesomeIcon class="power-state-icon" :class="className" :icon="icon" />
|
||||
<UiIcon :class="className" :icon="icon" class="power-state-icon" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
faMoon,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
faQuestion,
|
||||
faStop,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { PowerState } from "@/libs/xen-api";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -30,7 +30,7 @@ const icon = computed(() => icons[props.state] ?? faQuestion);
|
||||
const className = computed(() => `state-${props.state.toLocaleLowerCase()}`);
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style lang="postcss" scoped>
|
||||
.power-state-icon {
|
||||
color: var(--color-extra-blue-d60);
|
||||
|
||||
|
||||
63
@xen-orchestra/lite/src/components/ProgressBar.vue
Normal file
63
@xen-orchestra/lite/src/components/ProgressBar.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="progress-bar-component">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill" />
|
||||
</div>
|
||||
<div class="badge" v-if="label !== undefined">
|
||||
<span class="circle" />
|
||||
{{ label }}
|
||||
<UiBadge>{{ badgeLabel ?? progressWithUnit }}</UiBadge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
badgeLabel?: string | number;
|
||||
label?: string;
|
||||
maxValue?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxValue: 100,
|
||||
});
|
||||
|
||||
const progressWithUnit = computed(() => {
|
||||
const progress = Math.round((props.value / props.maxValue) * 100);
|
||||
return `${progress}%`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.badge {
|
||||
text-align: right;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.circle {
|
||||
display: inline-block;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
background-color: #716ac6;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
overflow: hidden;
|
||||
height: 1.2rem;
|
||||
border-radius: 0.4rem;
|
||||
background-color: var(--color-blue-scale-400);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
transition: width 1s ease-in-out;
|
||||
width: v-bind(progressWithUnit);
|
||||
height: 1.2rem;
|
||||
background-color: var(--color-extra-blue-d20);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="title-bar">
|
||||
<FontAwesomeIcon :icon="icon" class="icon" />
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<div class="title">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
icon: IconDefinition;
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="data.length !== 0">
|
||||
<div class="header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div v-if="data !== undefined">
|
||||
<div
|
||||
v-for="item in computedData.sortedArray"
|
||||
:key="item.id"
|
||||
class="progress-item"
|
||||
>
|
||||
<UiProgressBar :value="item.value" color="custom" />
|
||||
<div class="legend">
|
||||
<span class="circle" />
|
||||
{{ item.label }}
|
||||
<UiBadge class="badge">{{
|
||||
item.badgeLabel ?? `${item.value}%`
|
||||
}}</UiBadge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
|
||||
</div>
|
||||
<ProgressBar
|
||||
v-for="item in computedData.sortedArray"
|
||||
:key="item.id"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
:badge-label="item.badgeLabel"
|
||||
/>
|
||||
<div class="footer">
|
||||
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
|
||||
</div>
|
||||
<UiSpinner v-else class="spinner" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||
import UiProgressBar from "@/components/ui/UiProgressBar.vue";
|
||||
import { computed } from "vue";
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
|
||||
interface Data {
|
||||
id: string;
|
||||
@@ -41,7 +29,7 @@ interface Data {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data?: Data[];
|
||||
data: Array<Data>;
|
||||
nItems?: number;
|
||||
}
|
||||
|
||||
@@ -52,7 +40,7 @@ const computedData = computed(() => {
|
||||
let totalPercentUsage = 0;
|
||||
return {
|
||||
sortedArray: _data
|
||||
?.map((item) => {
|
||||
.map((item) => {
|
||||
const value = Math.round((item.value / (item.maxValue ?? 100)) * 100);
|
||||
totalPercentUsage += value;
|
||||
return {
|
||||
@@ -84,51 +72,23 @@ const computedData = computed(() => {
|
||||
font-size: 14px;
|
||||
color: var(--color-blue-scale-300);
|
||||
}
|
||||
</style>
|
||||
|
||||
.spinner {
|
||||
color: var(--color-extra-blue-base);
|
||||
display: flex;
|
||||
margin: auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
<style>
|
||||
.progress-bar-component:nth-of-type(2) .progress-bar-fill,
|
||||
.progress-bar-component:nth-of-type(2) .circle {
|
||||
background-color: var(--color-extra-blue-d60);
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin: 1.6em 0;
|
||||
.progress-bar-component:nth-of-type(3) .progress-bar-fill,
|
||||
.progress-bar-component:nth-of-type(3) .circle {
|
||||
background-color: var(--color-extra-blue-d40);
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.9em;
|
||||
font-weight: 700;
|
||||
.progress-bar-component:nth-of-type(4) .progress-bar-fill,
|
||||
.progress-bar-component:nth-of-type(4) .circle {
|
||||
background-color: var(--color-extra-blue-d20);
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
--progress-bar-height: 1.2rem;
|
||||
--progress-bar-color: var(--color-extra-blue-l20);
|
||||
--progress-bar-background-color: var(--color-blue-scale-400);
|
||||
}
|
||||
|
||||
.progress-item:nth-child(1) {
|
||||
--progress-bar-color: var(--color-extra-blue-d60);
|
||||
}
|
||||
|
||||
.progress-item:nth-child(2) {
|
||||
--progress-bar-color: var(--color-extra-blue-d40);
|
||||
}
|
||||
|
||||
.progress-item:nth-child(3) {
|
||||
--progress-bar-color: var(--color-extra-blue-d20);
|
||||
}
|
||||
|
||||
.circle {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--progress-bar-color);
|
||||
.progress-bar-component .progress-bar-fill,
|
||||
.progress-bar-component .circle {
|
||||
background-color: var(--color-extra-blue-l20);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div class="infra-action">
|
||||
<slot>
|
||||
<FontAwesomeIcon :icon="icon" fixed-width />
|
||||
<UiIcon :icon="icon" fixed-width />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
icon?: IconDefinition;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:icon="faServer"
|
||||
:route="{ name: 'host.dashboard', params: { uuid: host.uuid } }"
|
||||
>
|
||||
{{ host.name_label || "(Host)" }}
|
||||
{{ host.name_label || '(Host)' }}
|
||||
<template #actions>
|
||||
<InfraAction
|
||||
:icon="isExpanded ? faAngleDown : faAngleUp"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<a :href="href" class="link" @click="navigate">
|
||||
<FontAwesomeIcon :icon="icon" class="icon" />
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<div class="text">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -23,6 +23,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
icon: IconDefinition;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<li class="infra-loading-item">
|
||||
<div class="infra-item-label-placeholder">
|
||||
<div class="link-placeholder">
|
||||
<FontAwesomeIcon :icon="icon" class="icon" />
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<div class="loader"> </div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
icon: IconDefinition;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:icon="faDisplay"
|
||||
:route="{ name: 'vm.console', params: { uuid: vm.uuid } }"
|
||||
>
|
||||
{{ vm.name_label || "(VM)" }}
|
||||
{{ vm.name_label || '(VM)' }}
|
||||
<template #actions>
|
||||
<InfraAction>
|
||||
<PowerStateIcon :state="vm?.power_state" />
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiTitle type="h4">{{ $t("ram-usage") }}</UiTitle>
|
||||
<HostsRamUsage />
|
||||
<VmsRamUsage />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HostsRamUsage from "@/components/pool/dashboard/ramUsage/HostsRamUsage.vue";
|
||||
import VmsRamUsage from "@/components/pool/dashboard/ramUsage/VmsRamUsage.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
</script>
|
||||
@@ -14,7 +14,6 @@
|
||||
:label="$t('vms')"
|
||||
/>
|
||||
</template>
|
||||
<UiSpinner v-else class="spinner" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +22,6 @@ import { computed } from "vue";
|
||||
import PoolDashboardStatusItem from "@/components/pool/dashboard/PoolDashboardStatusItem.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiSeparator from "@/components/ui/UiSeparator.vue";
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
import { useHostMetricsStore } from "@/stores/host-metrics.store";
|
||||
import { useVmStore } from "@/stores/vm.store";
|
||||
@@ -47,13 +45,3 @@ const activeVmsCount = computed(() => {
|
||||
).length;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.spinner {
|
||||
color: var(--color-extra-blue-base);
|
||||
display: flex;
|
||||
margin: auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiTitle type="h4">{{ $t("storage-usage") }}</UiTitle>
|
||||
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="N_ITEMS">
|
||||
<UsageBar :data="data.result" :nItems="5">
|
||||
<template #header>
|
||||
<span>{{ $t("storage") }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
</template>
|
||||
<template #footer v-if="showFooter">
|
||||
<div class="footer-card">
|
||||
@@ -37,7 +37,6 @@ import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
import { formatSize, percent } from "@/libs/utils";
|
||||
import { useSrStore } from "@/stores/storage.store";
|
||||
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
|
||||
|
||||
const srStore = useSrStore();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
|
||||
<UsageBar :data="data" :n-items="5">
|
||||
<template #header>
|
||||
<span>{{ $t("hosts") }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
@@ -13,7 +13,6 @@ import UsageBar from "@/components/UsageBar.vue";
|
||||
import type { Stat } from "@/composables/fetch-stats.composable";
|
||||
import { getAvgCpuUsage } from "@/libs/utils";
|
||||
import type { HostStats } from "@/libs/xapi-stats";
|
||||
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
|
||||
|
||||
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
|
||||
"hostStats",
|
||||
@@ -43,10 +42,4 @@ const data = computed<{ id: string; label: string; value: number }[]>(() => {
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const statFetched: ComputedRef<boolean> = computed(() =>
|
||||
statFetched.value
|
||||
? true
|
||||
: stats.value.length > 0 && stats.value.length === data.value.length
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
|
||||
<UsageBar :data="data" :n-items="5">
|
||||
<template #header>
|
||||
<span>{{ $t("vms") }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
@@ -13,7 +13,6 @@ import UsageBar from "@/components/UsageBar.vue";
|
||||
import type { Stat } from "@/composables/fetch-stats.composable";
|
||||
import { getAvgCpuUsage } from "@/libs/utils";
|
||||
import type { VmStats } from "@/libs/xapi-stats";
|
||||
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
|
||||
|
||||
const stats = inject<ComputedRef<Stat<VmStats>[]>>(
|
||||
"vmStats",
|
||||
@@ -43,10 +42,4 @@ const data = computed<{ id: string; label: string; value: number }[]>(() => {
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const statFetched: ComputedRef<boolean> = computed(() =>
|
||||
statFetched.value
|
||||
? true
|
||||
: stats.value.length > 0 && stats.value.length === data.value.length
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
|
||||
<template #header>
|
||||
<span>{{ $t("hosts") }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
import UsageBar from "@/components/UsageBar.vue";
|
||||
import type { Stat } from "@/composables/fetch-stats.composable";
|
||||
import { formatSize, parseRamUsage } from "@/libs/utils";
|
||||
import type { HostStats } from "@/libs/xapi-stats";
|
||||
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
|
||||
|
||||
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
|
||||
"hostStats",
|
||||
computed(() => [])
|
||||
);
|
||||
|
||||
const data = computed(() => {
|
||||
const result: {
|
||||
id: string;
|
||||
label: string;
|
||||
value: number;
|
||||
badgeLabel: string;
|
||||
}[] = [];
|
||||
|
||||
stats.value.forEach((stat) => {
|
||||
if (stat.stats === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { percentUsed, total, used } = parseRamUsage(stat.stats);
|
||||
result.push({
|
||||
id: stat.id,
|
||||
label: stat.name,
|
||||
value: percentUsed,
|
||||
badgeLabel: `${formatSize(used)}/${formatSize(total)}`,
|
||||
});
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
const statFetched: ComputedRef<boolean> = computed(
|
||||
() =>
|
||||
statFetched.value ||
|
||||
(stats.value.length > 0 && stats.value.length === data.value.length)
|
||||
);
|
||||
</script>
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
|
||||
<template #header>
|
||||
<span>{{ $t("vms") }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
import UsageBar from "@/components/UsageBar.vue";
|
||||
import type { Stat } from "@/composables/fetch-stats.composable";
|
||||
import { formatSize, parseRamUsage } from "@/libs/utils";
|
||||
import type { VmStats } from "@/libs/xapi-stats";
|
||||
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
|
||||
|
||||
const stats = inject<ComputedRef<Stat<VmStats>[]>>(
|
||||
"vmStats",
|
||||
computed(() => [])
|
||||
);
|
||||
|
||||
const data = computed(() => {
|
||||
const result: {
|
||||
id: string;
|
||||
label: string;
|
||||
value: number;
|
||||
badgeLabel: string;
|
||||
}[] = [];
|
||||
|
||||
stats.value.forEach((stat) => {
|
||||
if (stat.stats === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { percentUsed, total, used } = parseRamUsage(stat.stats);
|
||||
result.push({
|
||||
id: stat.id,
|
||||
label: stat.name,
|
||||
value: percentUsed,
|
||||
badgeLabel: `${formatSize(used)}/${formatSize(total)}`,
|
||||
});
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
const statFetched: ComputedRef<boolean> = computed(
|
||||
() =>
|
||||
statFetched.value ||
|
||||
(stats.value.length > 0 && stats.value.length === data.value.length)
|
||||
);
|
||||
</script>
|
||||
@@ -1,27 +0,0 @@
|
||||
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { render } from "@testing-library/vue";
|
||||
|
||||
describe("UiBadge", () => {
|
||||
it("should render with no icon", () => {
|
||||
const { getByText, queryByTestId } = render(UiBadge, {
|
||||
slots: {
|
||||
default: "3456",
|
||||
},
|
||||
});
|
||||
|
||||
getByText("3456");
|
||||
|
||||
expect(queryByTestId("ui-icon")).toBeNull();
|
||||
});
|
||||
|
||||
it("should render with icon", () => {
|
||||
const { getByTestId } = render(UiBadge, {
|
||||
props: {
|
||||
icon: faDisplay,
|
||||
},
|
||||
});
|
||||
|
||||
getByTestId("ui-icon");
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,7 @@ defineProps<{
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
padding: 0 0.8rem;
|
||||
height: 1.8em;
|
||||
height: 2.4rem;
|
||||
color: var(--color-blue-scale-500);
|
||||
border-radius: 9.6rem;
|
||||
background-color: var(--color-blue-scale-300);
|
||||
|
||||
@@ -30,12 +30,7 @@ const props = withDefaults(
|
||||
transparent?: boolean;
|
||||
active?: boolean;
|
||||
}>(),
|
||||
{
|
||||
busy: undefined,
|
||||
disabled: undefined,
|
||||
outlined: undefined,
|
||||
transparent: undefined,
|
||||
}
|
||||
{ busy: undefined, disabled: undefined, outlined: undefined }
|
||||
);
|
||||
|
||||
const isGroupBusy = inject("isButtonGroupBusy", false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="{ merge }" class="ui-button-group">
|
||||
<div class="ui-button-group">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,7 +14,6 @@ const props = defineProps<{
|
||||
color?: Color;
|
||||
outlined?: boolean;
|
||||
transparent?: boolean;
|
||||
merge?: boolean;
|
||||
}>();
|
||||
provide(
|
||||
"isButtonGroupBusy",
|
||||
@@ -41,32 +40,8 @@ provide(
|
||||
<style lang="postcss" scoped>
|
||||
.ui-button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
&.merge {
|
||||
gap: 0;
|
||||
|
||||
:slotted(.ui-button) {
|
||||
&:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
&.outlined {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
&.outlined {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
<slot />
|
||||
</span>
|
||||
<span class="remove" @click.stop="emit('remove')">
|
||||
<FontAwesomeIcon :icon="faRemove" class="icon" />
|
||||
<UiIcon :icon="faRemove" class="icon" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faRemove } from "@fortawesome/free-solid-svg-icons";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "edit"): void;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<FontAwesomeIcon
|
||||
v-if="icon !== undefined"
|
||||
:fixed-width="fixedWidth"
|
||||
:icon="icon"
|
||||
:spin="busy"
|
||||
class="ui-icon"
|
||||
:fixed-width="fixedWidth"
|
||||
data-testid="ui-icon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
>
|
||||
<div class="container">
|
||||
<span v-if="onClose" class="close-icon" @click="emit('close')">
|
||||
<FontAwesomeIcon :icon="faXmark" />
|
||||
<UiIcon :icon="faXmark" />
|
||||
</span>
|
||||
<div v-if="icon || $slots.icon" class="modal-icon">
|
||||
<slot name="icon">
|
||||
<FontAwesomeIcon :icon="icon" />
|
||||
<UiIcon :icon="icon" />
|
||||
</slot>
|
||||
</div>
|
||||
<UiTitle v-if="$slots.title" type="h4">
|
||||
@@ -38,6 +38,7 @@ import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-progress-bar" :class="`color-${color}`">
|
||||
<div class="fill" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Color } from "@/types";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: number;
|
||||
color?: Color | "custom";
|
||||
maxValue?: number;
|
||||
}>(),
|
||||
{ color: "info", maxValue: 100 }
|
||||
);
|
||||
|
||||
const progressWithUnit = computed(() => {
|
||||
const progress = (props.value / props.maxValue) * 100;
|
||||
return `${progress}%`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-progress-bar {
|
||||
overflow: hidden;
|
||||
height: var(--progress-bar-height, 0.4rem);
|
||||
margin: 1rem 0;
|
||||
border-radius: 0.4rem;
|
||||
background-color: var(
|
||||
--progress-bar-background-color,
|
||||
var(--background-color-extra-blue)
|
||||
);
|
||||
|
||||
&.color-info {
|
||||
--progress-bar-color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
&.color-success {
|
||||
--progress-bar-color: var(--color-green-infra-base);
|
||||
}
|
||||
|
||||
&.color-warning {
|
||||
--progress-bar-color: var(--color-orange-world-base);
|
||||
}
|
||||
|
||||
&.color-error {
|
||||
--progress-bar-color: var(--color-red-vates-base);
|
||||
}
|
||||
}
|
||||
|
||||
.fill {
|
||||
width: v-bind(progressWithUnit);
|
||||
height: var(--progress-bar-height, 0.4rem);
|
||||
transition: width 1s ease-in-out;
|
||||
background-color: var(--progress-bar-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +1,23 @@
|
||||
# useBusy composable
|
||||
|
||||
```vue
|
||||
|
||||
<template>
|
||||
<span class="error" v-if="error">{{ error }}</span>
|
||||
<button @click="run" :disabled="isBusy">Do something</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useBusy from "@/composables/busy.composable";
|
||||
import useBusy from '@/composables/busy.composable';
|
||||
|
||||
async function doSomething() {
|
||||
try {
|
||||
// Doing some async work
|
||||
} catch (e) {
|
||||
throw "Something bad happened";
|
||||
async function doSomething() {
|
||||
try {
|
||||
// Doing some async work
|
||||
} catch (e) {
|
||||
throw "Something bad happened";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { isBusy, error, run } = useBusy(doSomething);
|
||||
const { isBusy, error, run } = useBusy(doSomething)
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import useBusy from "@/composables/busy.composable";
|
||||
import { expect, vi } from "vitest";
|
||||
|
||||
describe("Busy Composable", () => {
|
||||
it("should work", async () => {
|
||||
let resolve: (value: unknown) => void = () => undefined;
|
||||
const promise = new Promise((r) => (resolve = r));
|
||||
const func = vi.fn(() => promise);
|
||||
const { isBusy, run, error } = useBusy(func);
|
||||
|
||||
expect(isBusy.value).toBeFalsy();
|
||||
const runPromise = run();
|
||||
expect(isBusy.value).toBeTruthy();
|
||||
|
||||
resolve(null);
|
||||
await runPromise;
|
||||
expect(isBusy.value).toBeFalsy();
|
||||
expect(error.value).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle error", async () => {
|
||||
const errorMessage = "SOMETHING BAD HAPPENED";
|
||||
const promise = Promise.reject(errorMessage);
|
||||
const func = vi.fn(() => promise);
|
||||
const { isBusy, run, error } = useBusy(func);
|
||||
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
/**/
|
||||
}
|
||||
expect(isBusy.value).toBeFalsy();
|
||||
expect(error.value).toBe(errorMessage);
|
||||
});
|
||||
});
|
||||
@@ -13,23 +13,19 @@ const filteredCollection = myCollection.filter(predicate);
|
||||
By default, when adding/removing filters, the URL will update automatically.
|
||||
|
||||
```typescript
|
||||
addFilter("name:/^foo/i"); // Will update the URL with ?filter=name:/^foo/i
|
||||
addFilter('name:/^foo/i'); // Will update the URL with ?filter=name:/^foo/i
|
||||
```
|
||||
|
||||
### Change the URL query string parameter name
|
||||
|
||||
```typescript
|
||||
const {
|
||||
/* ... */
|
||||
} = useCollectionFilter({ queryStringParam: "f" }); // ?f=name:/^foo/i
|
||||
const { /* ... */ } = useCollectionFilter({ queryStringParam: 'f' }); // ?f=name:/^foo/i
|
||||
```
|
||||
|
||||
### Disable the usage of URL query string
|
||||
|
||||
```typescript
|
||||
const {
|
||||
/* ... */
|
||||
} = useCollectionFilter({ queryStringParam: undefined });
|
||||
const { /* ... */ } = useCollectionFilter({ queryStringParam: undefined });
|
||||
```
|
||||
|
||||
## Example of using the composable with the `CollectionFilter` component
|
||||
@@ -42,32 +38,32 @@ const {
|
||||
@add-filter="addFilter"
|
||||
@remove-filter="removeFilter"
|
||||
/>
|
||||
|
||||
|
||||
<div v-for="item in filteredCollection">...</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CollectionFilter from "@/components/CollectionFilter.vue";
|
||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||
import { computed } from "vue";
|
||||
import CollectionFilter from "@/components/CollectionFilter.vue";
|
||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||
import { computed } from "vue";
|
||||
|
||||
const collection = [
|
||||
{ name: "Foo", age: 5, registered: true },
|
||||
{ name: "Bar", age: 12, registered: false },
|
||||
{ name: "Foo Bar", age: 2, registered: true },
|
||||
{ name: "Bar Baz", age: 45, registered: false },
|
||||
{ name: "Foo Baz", age: 32, registered: false },
|
||||
{ name: "Foo Bar Baz", age: 32, registered: true },
|
||||
];
|
||||
const collection = [
|
||||
{ name: "Foo", age: 5, registered: true },
|
||||
{ name: "Bar", age: 12, registered: false },
|
||||
{ name: "Foo Bar", age: 2, registered: true },
|
||||
{ name: "Bar Baz", age: 45, registered: false },
|
||||
{ name: "Foo Baz", age: 32, registered: false },
|
||||
{ name: "Foo Bar Baz", age: 32, registered: true },
|
||||
];
|
||||
|
||||
const availableFilters: AvailableFilter[] = [
|
||||
{ property: "name", label: "Name", type: "string" },
|
||||
{ property: "age", label: "Age", type: "number" },
|
||||
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
|
||||
];
|
||||
const availableFilters: AvailableFilter[] = [
|
||||
{ property: "name", label: "Name", type: "string" },
|
||||
{ property: "age", label: "Age", type: "number" },
|
||||
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
|
||||
];
|
||||
|
||||
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
|
||||
|
||||
const filteredCollection = computed(() => collection.filter(predicate));
|
||||
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
|
||||
|
||||
const filteredCollection = computed(() => collection.filter(predicate));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||
import { expect } from "vitest";
|
||||
import { nextTick } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
vi.mock("vue-router", () => ({
|
||||
useRoute: vi.fn(() => ({
|
||||
query: {},
|
||||
})),
|
||||
useRouter: vi.fn(() => ({
|
||||
replace: () => undefined,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("Collection Filter Composable", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const items = [{ name: "Foo" }, { name: "Bar" }, { name: "Baz" }];
|
||||
|
||||
it("should add filters", () => {
|
||||
const { filters, addFilter } = useCollectionFilter();
|
||||
expect(filters.value).toHaveLength(0);
|
||||
addFilter("foo");
|
||||
addFilter("bar");
|
||||
expect(filters.value).toHaveLength(2);
|
||||
expect(filters.value).toEqual(["foo", "bar"]);
|
||||
});
|
||||
|
||||
it("should remove filters", () => {
|
||||
const { filters, addFilter, removeFilter } = useCollectionFilter();
|
||||
addFilter("foo");
|
||||
addFilter("bar");
|
||||
removeFilter("foo");
|
||||
expect(filters.value).toHaveLength(1);
|
||||
expect(filters.value).toEqual(["bar"]);
|
||||
});
|
||||
|
||||
it("should filter an array correctly", () => {
|
||||
const { addFilter, predicate } = useCollectionFilter();
|
||||
addFilter("name:/^Ba/");
|
||||
const filtered = items.filter(predicate.value);
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered).toEqual([{ name: "Bar" }, { name: "Baz" }]);
|
||||
});
|
||||
|
||||
it("should not use router when disabled", async () => {
|
||||
const replace = vi.fn();
|
||||
|
||||
vi.mocked(useRouter, { partial: true }).mockImplementation(() => ({
|
||||
replace,
|
||||
}));
|
||||
|
||||
const { addFilter } = useCollectionFilter({ queryStringParam: "" });
|
||||
|
||||
addFilter("name:/^Ba/");
|
||||
await nextTick();
|
||||
|
||||
expect(replace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use router when enabled", async () => {
|
||||
const replace = vi.fn();
|
||||
|
||||
vi.mocked(useRouter, { partial: true }).mockImplementation(() => ({
|
||||
replace,
|
||||
}));
|
||||
|
||||
const { addFilter } = useCollectionFilter();
|
||||
|
||||
addFilter("name:/^Ba/");
|
||||
await nextTick();
|
||||
|
||||
expect(replace).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should load single initial filter from query string", () => {
|
||||
vi.mocked(useRoute, { partial: true }).mockImplementation(() => ({
|
||||
query: {
|
||||
filter: "name:Foo",
|
||||
},
|
||||
}));
|
||||
const { filters } = useCollectionFilter();
|
||||
expect(filters.value).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should load multiple initial filters from query string", () => {
|
||||
vi.mocked(useRoute, { partial: true }).mockImplementation(() => ({
|
||||
query: {
|
||||
filter: "name:Foo age:20",
|
||||
},
|
||||
}));
|
||||
const { filters } = useCollectionFilter();
|
||||
expect(filters.value).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import useFilteredCollection from "./filtered-collection.composable";
|
||||
import useFilteredCollection from './filtered-collection.composable';
|
||||
|
||||
const players = [
|
||||
{ name: "Foo", team: "Blue" },
|
||||
{ name: "Bar", team: "Red" },
|
||||
{ name: "Baz", team: "Blue" },
|
||||
];
|
||||
|
||||
const bluePlayers = useFilteredCollection(
|
||||
players,
|
||||
(player) => player.team === "Blue"
|
||||
);
|
||||
const players = [
|
||||
{ name: "Foo", team: "Blue" },
|
||||
{ name: "Bar", team: "Red" },
|
||||
{ name: "Baz", team: "Blue" },
|
||||
]
|
||||
|
||||
const bluePlayers = useFilteredCollection(players, (player) => player.team === "Blue");
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -5,28 +5,27 @@
|
||||
<div v-for="item in items">
|
||||
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
|
||||
</div>
|
||||
|
||||
|
||||
<UiModal v-if="isRemoveModalOpen">
|
||||
Are you sure you want to delete {{ removeModalPayload.name }}
|
||||
|
||||
<button @click="handleRemove">Yes</button>
|
||||
<button @click="closeRemoveModal">No</button>
|
||||
|
||||
<button @click="handleRemove">Yes</button> <button @click="closeRemoveModal">No</button>
|
||||
</UiModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import useModal from '@/composables/modal.composable';
|
||||
|
||||
const {
|
||||
payload: removeModalPayload,
|
||||
isOpen: isRemoveModalOpen,
|
||||
open: openRemoveModal,
|
||||
close: closeRemoveModal,
|
||||
} = useModal();
|
||||
|
||||
async function handleRemove() {
|
||||
await removeItem(removeModalPayload.id);
|
||||
closeRemoveModal();
|
||||
}
|
||||
const {
|
||||
payload: removeModalPayload,
|
||||
isOpen: isRemoveModalOpen,
|
||||
open: openRemoveModal,
|
||||
close: closeRemoveModal,
|
||||
} = useModal()
|
||||
|
||||
async function handleRemove() {
|
||||
await removeItem(removeModalPayload.id);
|
||||
closeRemoveModal()
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -4,30 +4,34 @@
|
||||
<template>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" v-model="areAllSelected" />
|
||||
</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" v-model="areAllSelected">
|
||||
</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in items">
|
||||
<td>
|
||||
<input type="checkbox" :value="item.id" v-model="selected" />
|
||||
</td>
|
||||
<td>{{ item.name }}</td>
|
||||
</tr>
|
||||
<tr v-for="item in items">
|
||||
<td>
|
||||
<input type="checkbox" :value="item.id" v-model="selected" />
|
||||
</td>
|
||||
<td>{{ item.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- You can use something else than a "Select All" checkbox -->
|
||||
<button @click="areAllSelected = !areAllSelected">Toggle all selected</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useMultiSelect from "./multi-select.composable";
|
||||
|
||||
const { selected, areAllSelected } = useMultiSelect();
|
||||
<script lang="ts" setup>
|
||||
import useMultiSelect from './multi-select.composable';
|
||||
|
||||
const {
|
||||
selected,
|
||||
areAllSelected,
|
||||
} = useMultiSelect()
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -123,37 +123,3 @@ export const buildXoObject = (
|
||||
...record,
|
||||
$ref: params.opaqueRef,
|
||||
});
|
||||
|
||||
export function parseRamUsage(
|
||||
{
|
||||
memory,
|
||||
memoryFree,
|
||||
}: {
|
||||
memory: number[];
|
||||
memoryFree?: number[];
|
||||
},
|
||||
{ nSequence = 4 } = {}
|
||||
) {
|
||||
const _nSequence = Math.min(memory.length, nSequence);
|
||||
|
||||
let total = 0;
|
||||
let used = 0;
|
||||
|
||||
memory = memory.slice(memory.length - _nSequence);
|
||||
memoryFree = memoryFree?.slice(memoryFree.length - _nSequence);
|
||||
|
||||
memory.forEach((ram, key) => {
|
||||
total += ram;
|
||||
used += ram - (memoryFree?.[key] ?? 0);
|
||||
});
|
||||
|
||||
const percentUsed = percent(used, total);
|
||||
return {
|
||||
// In case `memoryFree` is not given by the xapi,
|
||||
// we won't be able to calculate the percentage of used memory properly.
|
||||
percentUsed:
|
||||
memoryFree === undefined || isNaN(percentUsed) ? 0 : percentUsed,
|
||||
total: total / _nSequence,
|
||||
used: memoryFree === undefined ? 0 : used / _nSequence,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ export type VmStats = {
|
||||
w: Record<string, number[]>;
|
||||
};
|
||||
memory: number[];
|
||||
memoryFree?: number[];
|
||||
memoryFree: number[];
|
||||
vifs: {
|
||||
rx: Record<string, number[]>;
|
||||
tx: Record<string, number[]>;
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
"community-name": "{name} community",
|
||||
"copy": "Copy",
|
||||
"cpu-usage":"CPU usage",
|
||||
"theme-dark": "Dark",
|
||||
"theme-light": "Light",
|
||||
"theme-auto": "Auto",
|
||||
"dark-mode": "Dark mode",
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
"descending": "descending",
|
||||
@@ -39,7 +37,6 @@
|
||||
"or": "Or",
|
||||
"password": "Password",
|
||||
"property": "Property",
|
||||
"ram-usage":"RAM usage",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
"settings": "Settings",
|
||||
"snapshot": "Snapshot",
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
"community-name": "Communauté {name}",
|
||||
"copy": "Copier",
|
||||
"cpu-usage":"Utilisation CPU",
|
||||
"theme-dark": "Sombre",
|
||||
"theme-light": "Clair",
|
||||
"theme-auto": "Auto",
|
||||
"dark-mode": "Mode sombre",
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"descending": "descendant",
|
||||
@@ -39,7 +37,6 @@
|
||||
"or": "Ou",
|
||||
"password": "Mot de passe",
|
||||
"property": "Propriété",
|
||||
"ram-usage":"Utilisation de la RAM",
|
||||
"send-us-feedback": "Envoyez-nous vos commentaires",
|
||||
"settings": "Paramètres",
|
||||
"snapshot": "Instantané",
|
||||
|
||||
@@ -3,13 +3,11 @@ import { createApp } from "vue";
|
||||
import App from "@/App.vue";
|
||||
import i18n from "@/i18n";
|
||||
import router from "@/router";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(i18n);
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.component("FontAwesomeIcon", FontAwesomeIcon);
|
||||
|
||||
app.mount("#root");
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { sortRecordsByNameLabel } from "@/libs/utils";
|
||||
import type { GRANULARITY } from "@/libs/xapi-stats";
|
||||
import type { XenApiHost } from "@/libs/xen-api";
|
||||
import { createRecordContext } from "@/stores/index";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useHostStore = defineStore("host", () => {
|
||||
const xapiStats = useXenApiStore().getXapiStats();
|
||||
const recordContext = createRecordContext<XenApiHost>("host", {
|
||||
sort: sortRecordsByNameLabel,
|
||||
});
|
||||
@@ -15,7 +16,7 @@ export const useHostStore = defineStore("host", () => {
|
||||
if (host === undefined) {
|
||||
throw new Error(`Host ${id} could not be found.`);
|
||||
}
|
||||
return useXenApiStore().getXapiStats()._getAndUpdateStats({
|
||||
return xapiStats._getAndUpdateStats({
|
||||
host,
|
||||
uuid: host.uuid,
|
||||
granularity,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { useColorMode } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
export const useUiStore = defineStore("ui", () => {
|
||||
const currentHostOpaqueRef = ref();
|
||||
|
||||
const colorMode = useColorMode({ emitAuto: true, initialValue: "dark" });
|
||||
|
||||
return {
|
||||
colorMode,
|
||||
currentHostOpaqueRef,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { sortRecordsByNameLabel } from "@/libs/utils";
|
||||
import type { GRANULARITY } from "@/libs/xapi-stats";
|
||||
import type { XenApiVm } from "@/libs/xen-api";
|
||||
import { useHostStore } from "@/stores/host.store";
|
||||
import { createRecordContext } from "@/stores/index";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed } from "vue";
|
||||
|
||||
export const useVmStore = defineStore("vm", () => {
|
||||
const hostStore = useHostStore();
|
||||
const xapiStats = useXenApiStore().getXapiStats();
|
||||
const baseVmContext = createRecordContext<XenApiVm>("VM", {
|
||||
filter: (vm) =>
|
||||
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain,
|
||||
@@ -41,7 +42,7 @@ export const useVmStore = defineStore("vm", () => {
|
||||
throw new Error(`VM ${id} is halted or host could not be found.`);
|
||||
}
|
||||
|
||||
return useXenApiStore().getXapiStats()._getAndUpdateStats({
|
||||
return xapiStats._getAndUpdateStats({
|
||||
host,
|
||||
uuid: vm.uuid,
|
||||
granularity,
|
||||
|
||||
@@ -3,18 +3,13 @@
|
||||
<PoolDashboardStatus class="item" />
|
||||
<PoolDashboardStorageUsage class="item" />
|
||||
<PoolDashboardCpuUsage class="item" />
|
||||
<PoolDashboardRamUsage class="item" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export const N_ITEMS = 5;
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { differenceBy } from "lodash-es";
|
||||
import { computed, onMounted, provide, watch } from "vue";
|
||||
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
|
||||
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
|
||||
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
||||
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
|
||||
import useFetchStats from "@/composables/fetch-stats.composable";
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
rel="noopener noreferrer"
|
||||
href="https://xcp-ng.org/blog/"
|
||||
>{{ $t("news-name", { name: "XCP-ng" }) }}</a
|
||||
>
|
||||
-
|
||||
<a
|
||||
> - <a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://xen-orchestra.com/blog/"
|
||||
@@ -37,9 +35,7 @@
|
||||
rel="noopener noreferrer"
|
||||
href="https://xcp-ng.org/forum"
|
||||
>{{ $t("community-name", { name: "XCP-ng" }) }}</a
|
||||
>
|
||||
-
|
||||
<a
|
||||
> - <a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://xcp-ng.org/forum/category/12/xen-orchestra"
|
||||
@@ -54,15 +50,14 @@
|
||||
<UiKeyValueList>
|
||||
<UiKeyValueRow>
|
||||
<template #key>{{ $t("appearance") }}</template>
|
||||
<template #value>
|
||||
<FormLabel>
|
||||
<FormSelect v-model="colorMode">
|
||||
<option value="auto">{{ $t("theme-auto") }}</option>
|
||||
<option value="dark">{{ $t("theme-dark") }}</option>
|
||||
<option value="light">{{ $t("theme-light") }}</option>
|
||||
</FormSelect>
|
||||
</FormLabel>
|
||||
</template>
|
||||
<template #value
|
||||
><FormLabel>
|
||||
<FormToggle
|
||||
:modelValue="darkMode"
|
||||
@update:modelValue="setDarkMode"
|
||||
/>{{ $t("dark-mode") }}</FormLabel
|
||||
></template
|
||||
>
|
||||
</UiKeyValueRow>
|
||||
</UiKeyValueList>
|
||||
</UiCard>
|
||||
@@ -90,16 +85,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { watch } from "vue";
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { locales } from "@/i18n";
|
||||
import { faEarthAmericas, faGear } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import TitleBar from "@/components/TitleBar.vue";
|
||||
import FormLabel from "@/components/form/FormLabel.vue";
|
||||
import FormToggle from "@/components/form/FormToggle.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
|
||||
import UiKeyValueRow from "@/components/ui/UiKeyValueRow.vue";
|
||||
@@ -111,7 +105,12 @@ const { locale } = useI18n();
|
||||
|
||||
watch(locale, (newLocale) => localStorage.setItem("lang", newLocale));
|
||||
|
||||
const { colorMode } = storeToRefs(useUiStore());
|
||||
const colorMode = useLocalStorage<string>("colorMode", "dark");
|
||||
const darkMode = computed(() => colorMode.value !== "light");
|
||||
const setDarkMode = (enabled: boolean) => {
|
||||
colorMode.value = enabled ? "dark" : "light";
|
||||
document.documentElement.classList[enabled ? "add" : "remove"]("dark");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["ES2019", "dom"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
{
|
||||
"files": [],
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["ES2019"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node", "vitest/globals"]
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
/// <reference types="vitest" />
|
||||
import { URL, fileURLToPath } from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import vueI18n from "@intlify/vite-plugin-vue-i18n";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { fileURLToPath, URL } from "url";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 3000,
|
||||
},
|
||||
plugins: [vue(), vueI18n()],
|
||||
define: {
|
||||
XO_LITE_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
@@ -30,13 +25,4 @@ export default defineConfig({
|
||||
optimizeDeps: {
|
||||
include: ["complex-matcher"],
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "happy-dom",
|
||||
exclude: ["e2e"],
|
||||
coverage: {
|
||||
reportsDirectory: ".tests-output/coverage",
|
||||
},
|
||||
},
|
||||
root: ".",
|
||||
});
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const escapeRegExp = require('lodash/escapeRegExp')
|
||||
|
||||
const compileGlobPatternFragment = pattern => pattern.split('*').map(escapeRegExp).join('.*')
|
||||
|
||||
module.exports = function compileGlobPattern(pattern) {
|
||||
const no = []
|
||||
const yes = []
|
||||
pattern.split(/[\s,]+/).forEach(pattern => {
|
||||
if (pattern[0] === '-') {
|
||||
no.push(pattern.slice(1))
|
||||
} else {
|
||||
yes.push(pattern)
|
||||
}
|
||||
})
|
||||
|
||||
const raw = ['^']
|
||||
|
||||
if (no.length !== 0) {
|
||||
raw.push('(?!', no.map(compileGlobPatternFragment).join('|'), ')')
|
||||
}
|
||||
|
||||
if (yes.length !== 0) {
|
||||
raw.push('(?:', yes.map(compileGlobPatternFragment).join('|'), ')')
|
||||
} else {
|
||||
raw.push('.*')
|
||||
}
|
||||
|
||||
raw.push('$')
|
||||
|
||||
return new RegExp(raw.join(''))
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const compileGlobPattern = require('./_compileGlobPattern.js')
|
||||
const createConsoleTransport = require('./transports/console')
|
||||
const { LEVELS, resolve } = require('./levels')
|
||||
const { compileGlobPattern } = require('./utils')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
const compileGlobPattern = require('./_compileGlobPattern.js')
|
||||
const createTransport = require('./transports/console')
|
||||
const { LEVELS, resolve } = require('./levels')
|
||||
|
||||
@@ -9,19 +8,8 @@ if (!(symbol in global)) {
|
||||
// the default behavior, without requiring `configure` is to avoid
|
||||
// logging anything unless it's a real error
|
||||
const transport = createTransport()
|
||||
|
||||
const { env } = process
|
||||
|
||||
const pattern = [env.DEBUG, env.NODE_DEBUG].filter(Boolean).join(',')
|
||||
const matchDebug = pattern.length !== 0 ? RegExp.prototype.test.bind(compileGlobPattern(pattern)) : () => false
|
||||
|
||||
const level = resolve(env.LOG_LEVEL, LEVELS.WARN)
|
||||
|
||||
global[symbol] = function conditionalTransport(log) {
|
||||
if (log.level >= level || matchDebug(log.namespace)) {
|
||||
transport(log)
|
||||
}
|
||||
}
|
||||
const level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
|
||||
global[symbol] = log => log.level >= level && transport(log)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/log",
|
||||
"version": "0.5.0",
|
||||
"version": "0.4.0",
|
||||
"license": "ISC",
|
||||
"description": "Logging system with decoupled producers/consumer",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const fromCallback = require('promise-toolbox/fromCallback')
|
||||
// eslint-disable-next-line n/no-extraneous-require
|
||||
// eslint-disable-next-line n/no-missing-require
|
||||
const splitHost = require('split-host')
|
||||
// eslint-disable-next-line n/no-extraneous-require
|
||||
// eslint-disable-next-line n/no-missing-require
|
||||
const { createClient, Facility, Severity, Transport } = require('syslog-client')
|
||||
|
||||
const LEVELS = require('../levels')
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const escapeRegExp = require('lodash/escapeRegExp')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const TPL_RE = /\{\{(.+?)\}\}/g
|
||||
const evalTemplate = (tpl, data) => {
|
||||
const getData = typeof data === 'function' ? (_, key) => data(key) : (_, key) => data[key]
|
||||
@@ -10,6 +14,39 @@ exports.evalTemplate = evalTemplate
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const compileGlobPatternFragment = pattern => pattern.split('*').map(escapeRegExp).join('.*')
|
||||
|
||||
const compileGlobPattern = pattern => {
|
||||
const no = []
|
||||
const yes = []
|
||||
pattern.split(/[\s,]+/).forEach(pattern => {
|
||||
if (pattern[0] === '-') {
|
||||
no.push(pattern.slice(1))
|
||||
} else {
|
||||
yes.push(pattern)
|
||||
}
|
||||
})
|
||||
|
||||
const raw = ['^']
|
||||
|
||||
if (no.length !== 0) {
|
||||
raw.push('(?!', no.map(compileGlobPatternFragment).join('|'), ')')
|
||||
}
|
||||
|
||||
if (yes.length !== 0) {
|
||||
raw.push('(?:', yes.map(compileGlobPatternFragment).join('|'), ')')
|
||||
} else {
|
||||
raw.push('.*')
|
||||
}
|
||||
|
||||
raw.push('$')
|
||||
|
||||
return new RegExp(raw.join(''))
|
||||
}
|
||||
exports.compileGlobPattern = compileGlobPattern
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const required = name => {
|
||||
throw new Error(`missing required arg ${name}`)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('assert').strict
|
||||
|
||||
const compileGlobPattern = require('./_compileGlobPattern.js')
|
||||
const { compileGlobPattern } = require('./utils')
|
||||
|
||||
describe('compileGlobPattern()', () => {
|
||||
it('works', () => {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
- mixins can depend on each other, they will be instanciated on-demand
|
||||
|
||||
```js
|
||||
import mixin from '@xen-orchestra/mixin'
|
||||
|
||||
class MyMixin {
|
||||
constructor(app, ...mixinParams) {}
|
||||
|
||||
foo() {}
|
||||
}
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
mixin(this, { MyMixin }, [...mixinParams])
|
||||
}
|
||||
}
|
||||
|
||||
app = new App()
|
||||
app.myMixin.foo()
|
||||
```
|
||||
|
||||
@@ -12,29 +12,6 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/mixin
|
||||
> npm install --save @xen-orchestra/mixin
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
- mixins can depend on each other, they will be instanciated on-demand
|
||||
|
||||
```js
|
||||
import mixin from '@xen-orchestra/mixin'
|
||||
|
||||
class MyMixin {
|
||||
constructor(app, ...mixinParams) {}
|
||||
|
||||
foo() {}
|
||||
}
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
mixin(this, { MyMixin }, [...mixinParams])
|
||||
}
|
||||
}
|
||||
|
||||
app = new App()
|
||||
app.myMixin.foo()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.1",
|
||||
"engines": {
|
||||
"node": ">=15.6"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"@vates/event-listeners-manager": "^1.0.1",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/emit-async": "^1.0.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"acme-client": "^5.0.0",
|
||||
"app-conf": "^2.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/proxy",
|
||||
"version": "0.26.6",
|
||||
"version": "0.26.4",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "XO Proxy used to remotely execute backup jobs",
|
||||
"keywords": [
|
||||
@@ -30,15 +30,15 @@
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.3",
|
||||
"@vates/disposable": "^0.1.2",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.2",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/backups": "^0.29.0",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/mixin": "^0.1.0",
|
||||
"@xen-orchestra/mixins": "^0.8.2",
|
||||
"@xen-orchestra/mixins": "^0.8.1",
|
||||
"@xen-orchestra/self-signed": "^0.1.3",
|
||||
"@xen-orchestra/xapi": "^1.5.3",
|
||||
"@xen-orchestra/xapi": "^1.5.2",
|
||||
"ajv": "^8.0.3",
|
||||
"app-conf": "^2.3.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"pw": "^0.0.4",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-vmdk-to-vhd": "^2.5.0"
|
||||
"xo-vmdk-to-vhd": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/xapi",
|
||||
"version": "1.5.3",
|
||||
"version": "1.5.2",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
@@ -23,14 +23,14 @@
|
||||
"dependencies": {
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"json-rpc-protocol": "^0.13.2",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.2.0",
|
||||
"vhd-lib": "^4.1.1",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"private": false,
|
||||
|
||||
80
CHANGELOG.md
80
CHANGELOG.md
@@ -1,87 +1,11 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.77.1** (2022-12-07)
|
||||
## **5.76.1** (2022-11-08)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Backups] Automatically detect, report and fix cache inconsistencies
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Warm migration] Fix start and delete VMs after a warm migration [#6568](https://github.com/vatesfr/xen-orchestra/issues/6568)
|
||||
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/backups 0.29.2
|
||||
- @xen-orchestra/proxy 0.26.6
|
||||
- xo-server 0.107.2
|
||||
|
||||
## **5.77.0** (2022-11-30)
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Proxies] Ability to register an existing proxy (PR [#6556](https://github.com/vatesfr/xen-orchestra/pull/6556))
|
||||
- [VM] [Warm migration](https://xen-orchestra.com/blog/warm-migration-with-xen-orchestra/) support (PRs [6549](https://github.com/vatesfr/xen-orchestra/pull/6549) & [6549](https://github.com/vatesfr/xen-orchestra/pull/6549))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Remotes] Prevent remote path from ending with `xo-vm-backups` as it's usually a mistake
|
||||
- [OVA export] Speed up OVA generation by 2. Generated file will be bigger (as big as uncompressed XVA) (PR [#6487](https://github.com/vatesfr/xen-orchestra/pull/6487))
|
||||
- [Settings/Users] Add `Remove` button to delete OTP of users from the admin panel [Forum#6521](https://xcp-ng.org/forum/topic/6521/remove-totp-on-a-user-account) (PR [#6541](https://github.com/vatesfr/xen-orchestra/pull/6541))
|
||||
- [Plugin/transport-nagios] XO now reports backed up VMs invidually with the VM name label used as _host_ and backup job name used as _service_
|
||||
- [VM/Advanced] Add warm migration button (PR [#6533](https://github.com/vatesfr/xen-orchestra/pull/6533))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Dashboard/Health] Fix `Unknown SR` and `Unknown VDI` in Unhealthy VDIs (PR [#6519](https://github.com/vatesfr/xen-orchestra/pull/6519))
|
||||
- [Delta Backup] Can now recover VHD merge when failed at the begining
|
||||
- [Delta Backup] Fix `ENOENT` errors when merging a VHD directory on non-S3 remote
|
||||
- [Remote] Prevent the browser from auto-completing the encryption key field
|
||||
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/log 0.5.0
|
||||
- @vates/disposable 0.1.3
|
||||
- @xen-orchestra/fs 3.3.0
|
||||
- vhd-lib 4.2.0
|
||||
- @xen-orchestra/audit-core 0.2.2
|
||||
- @xen-orchestra/backups 0.29.1
|
||||
- @xen-orchestra/backups-cli 1.0.0
|
||||
- @xen-orchestra/mixins 0.8.2
|
||||
- @xen-orchestra/xapi 1.5.3
|
||||
- @xen-orchestra/proxy 0.26.5
|
||||
- xo-vmdk-to-vhd 2.5.0
|
||||
- xo-cli 0.14.2
|
||||
- xo-server 5.107.1
|
||||
- xo-server-audit 0.10.2
|
||||
- xo-server-auth-ldap 0.10.6
|
||||
- xo-server-backup-reports 0.17.2
|
||||
- xo-server-load-balancer 0.7.2
|
||||
- xo-server-netbox 0.3.5
|
||||
- xo-server-sdn-controller 1.0.7
|
||||
- xo-server-transport-nagios 1.0.0
|
||||
- xo-server-usage-report 0.10.2
|
||||
- xo-server-web-hooks 0.3.2
|
||||
- xo-web 5.108.0
|
||||
|
||||
## **5.76.2** (2022-11-14)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Proxies] Fix `this.getObject is not a function` on upgrade
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server 5.106.1
|
||||
|
||||
## **5.76.1** (2022-11-08)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [API] `proxy.register` accepts `vmUuid` parameter which can be used when not connected to the XAPI containing the XO Proxy VM
|
||||
- [Proxy] Can now upgrade proxies in VMs not connected to XO
|
||||
- [REST API] Expose VM snapshots and templates
|
||||
@@ -148,6 +72,8 @@
|
||||
|
||||
## **5.75.0** (2022-09-30)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Backup/Restore file] Implement File level restore for s3 and encrypted backups (PR [#6409](https://github.com/vatesfr/xen-orchestra/pull/6409))
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Dashboard/Health] Fix `Unknown SR` and `Unknown VDI` in Unhealthy VDIs (PR [#6519](https://github.com/vatesfr/xen-orchestra/pull/6519))
|
||||
- [Proxies] Fix `this.getObject is not a function` on upgrade
|
||||
|
||||
### Packages to release
|
||||
|
||||
> When modifying a package, add it here with its release type.
|
||||
@@ -27,4 +30,6 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user