Compare commits
43 Commits
lite/ui-ic
...
ci-prepare
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
167dfd2c40 | ||
|
|
36f7f193aa | ||
|
|
ca4a82ec38 | ||
|
|
37aea1888d | ||
|
|
92f3b4ddd7 | ||
|
|
647995428c | ||
|
|
407e9c25f3 | ||
|
|
1612ab7335 | ||
|
|
b952c36210 | ||
|
|
96b5cb2c61 | ||
|
|
c5b3acfce2 | ||
|
|
20a01bf266 | ||
|
|
a33b88cf1c | ||
|
|
09a2f45ada | ||
|
|
83a7dd7ea1 | ||
|
|
afc1b6a5c0 | ||
|
|
7f4f860735 | ||
|
|
d789e3aa0d | ||
|
|
f5b91cd45d | ||
|
|
92ab4b3309 | ||
|
|
2c456e4c89 | ||
|
|
1460e63449 | ||
|
|
8291124c1f | ||
|
|
fc4d9accfd | ||
|
|
80969b785f | ||
|
|
3dfd7f1835 | ||
|
|
65daa39ebe | ||
|
|
5ad94504e3 | ||
|
|
4101bf3ba5 | ||
|
|
e9d52864ef | ||
|
|
aef2696426 | ||
|
|
94c755b102 | ||
|
|
279b457348 | ||
|
|
b5988bb8b7 | ||
|
|
f73b1d8b40 | ||
|
|
b2ccb07a95 | ||
|
|
9560cc4e33 | ||
|
|
e87c380556 | ||
|
|
b0846876f7 | ||
|
|
477ed67957 | ||
|
|
5acacd7e1e | ||
|
|
8d542fe9c0 | ||
|
|
b0cb249ae9 |
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -30,6 +30,7 @@ if (args.length === 0) {
|
||||
|
||||
${name} v${version}
|
||||
`)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use strict'
|
||||
import { readFileSync } from 'fs'
|
||||
import getopts from 'getopts'
|
||||
|
||||
const getopts = require('getopts')
|
||||
const { version } = JSON.parse(readFileSync(new URL('package.json', import.meta.url)))
|
||||
|
||||
const { version } = require('./package.json')
|
||||
|
||||
module.exports = commands =>
|
||||
async function (args, prefix) {
|
||||
export function composeCommands(commands) {
|
||||
return async function (args, prefix) {
|
||||
const opts = getopts(args, {
|
||||
alias: {
|
||||
help: 'h',
|
||||
@@ -30,5 +29,6 @@ xo-backups v${version}
|
||||
return
|
||||
}
|
||||
|
||||
return command.main(args.slice(1), prefix + ' ' + commandName)
|
||||
return (await command.default)(args.slice(1), prefix + ' ' + commandName)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
'use strict'
|
||||
import fs from 'fs/promises'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const { dirname } = require('path')
|
||||
export * from 'fs/promises'
|
||||
|
||||
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
|
||||
module.exports = fs
|
||||
|
||||
fs.getSize = path =>
|
||||
export const getSize = path =>
|
||||
fs.stat(path).then(
|
||||
_ => _.size,
|
||||
error => {
|
||||
@@ -16,7 +14,7 @@ fs.getSize = path =>
|
||||
}
|
||||
)
|
||||
|
||||
fs.mktree = async function mkdirp(path) {
|
||||
export async function mktree(path) {
|
||||
try {
|
||||
await fs.mkdir(path)
|
||||
} catch (error) {
|
||||
@@ -26,8 +24,8 @@ fs.mktree = async function mkdirp(path) {
|
||||
return
|
||||
}
|
||||
if (code === 'ENOENT') {
|
||||
await mkdirp(dirname(path))
|
||||
return mkdirp(path)
|
||||
await mktree(dirname(path))
|
||||
return mktree(path)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -37,7 +35,7 @@ fs.mktree = async function mkdirp(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
|
||||
fs.readdir2 = path =>
|
||||
export const readdir2 = path =>
|
||||
fs.readdir(path).then(
|
||||
entries => {
|
||||
entries.forEach((entry, i) => {
|
||||
@@ -59,7 +57,7 @@ fs.readdir2 = path =>
|
||||
}
|
||||
)
|
||||
|
||||
fs.symlink2 = async (target, path) => {
|
||||
export async function symlink2(target, path) {
|
||||
try {
|
||||
await fs.symlink(target, path)
|
||||
} catch (error) {
|
||||
@@ -1,40 +0,0 @@
|
||||
'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)
|
||||
}
|
||||
})
|
||||
}
|
||||
38
@xen-orchestra/backups-cli/commands/clean-vms.mjs
Normal file
38
@xen-orchestra/backups-cli/commands/clean-vms.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
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,13 +1,10 @@
|
||||
'use strict'
|
||||
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'
|
||||
|
||||
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]) {
|
||||
export default async function createSymlinkIndex([backupDir, fieldPath]) {
|
||||
const indexDir = join(backupDir, 'indexes', filenamify(fieldPath))
|
||||
await mktree(indexDir)
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
'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')
|
||||
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'
|
||||
|
||||
const sha512 = str => createHash('sha512').update(str).digest('hex')
|
||||
const sum = values => values.reduce((a, b) => a + b)
|
||||
|
||||
module.exports = async function info(vmDirs) {
|
||||
export default async function info(vmDirs) {
|
||||
const jsonFiles = (
|
||||
await asyncMap(vmDirs, async vmDir => (await readdir2(vmDir)).filter(_ => _.endsWith('.json')))
|
||||
).flat()
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { composeCommands } from './_composeCommands.mjs'
|
||||
|
||||
'use strict'
|
||||
const importDefault = async path => (await import(path)).default
|
||||
|
||||
require('./_composeCommands')({
|
||||
composeCommands({
|
||||
'clean-vms': {
|
||||
get main() {
|
||||
return require('./commands/clean-vms')
|
||||
get default() {
|
||||
return importDefault('./commands/clean-vms.mjs')
|
||||
},
|
||||
usage: `[--fix] [--merge] [--remove] xo-vm-backups/*
|
||||
|
||||
@@ -18,14 +19,14 @@ require('./_composeCommands')({
|
||||
`,
|
||||
},
|
||||
'create-symlink-index': {
|
||||
get main() {
|
||||
return require('./commands/create-symlink-index')
|
||||
get default() {
|
||||
return importDefault('./commands/create-symlink-index.mjs')
|
||||
},
|
||||
usage: 'xo-vm-backups <field path>',
|
||||
},
|
||||
info: {
|
||||
get main() {
|
||||
return require('./commands/info')
|
||||
get default() {
|
||||
return importDefault('./commands/info.mjs')
|
||||
},
|
||||
usage: 'xo-vm-backups/*',
|
||||
},
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"bin": {
|
||||
"xo-backups": "index.js"
|
||||
"xo-backups": "index.mjs"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
@@ -12,10 +12,10 @@
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0"
|
||||
"promise-toolbox":"^0.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.10.1"
|
||||
"node": ">=14"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups-cli",
|
||||
"name": "@xen-orchestra/backups-cli",
|
||||
|
||||
@@ -537,10 +537,6 @@ class RemoteAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateVmBackupListCache(vmUuid) {
|
||||
await this.handler.unlink(this.#getVmBackupsCache(vmUuid))
|
||||
}
|
||||
|
||||
async #getCachabledDataListVmBackups(dir) {
|
||||
debug('generating cache', { path: dir })
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ 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,
|
||||
|
||||
@@ -284,15 +284,24 @@ export default class RemoteHandlerAbstract {
|
||||
return this._encryptor.decryptData(data)
|
||||
}
|
||||
|
||||
async rename(oldPath, newPath, { checksum = false } = {}) {
|
||||
oldPath = normalizePath(oldPath)
|
||||
newPath = normalizePath(newPath)
|
||||
|
||||
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
|
||||
if (checksum) {
|
||||
p = Promise.all([p, this._rename(checksumFile(oldPath), checksumFile(newPath))])
|
||||
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)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
rename(oldPath, newPath, { checksum = false } = {}) {
|
||||
return this.#rename(normalizePath(oldPath), normalizePath(newPath), { checksum })
|
||||
}
|
||||
|
||||
async copy(oldPath, newPath, { checksum = false } = {}) {
|
||||
|
||||
@@ -228,6 +228,17 @@ 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()', () => {
|
||||
|
||||
@@ -1,15 +1,2 @@
|
||||
module.exports = {
|
||||
importOrder: [
|
||||
"^[^/]+$",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^@/components/(.*)$",
|
||||
"^@/composables/(.*)$",
|
||||
"^@/libs/(.*)$",
|
||||
"^@/router/(.*)$",
|
||||
"^@/stores/(.*)$",
|
||||
"^@/views/(.*)$",
|
||||
],
|
||||
importOrderSeparation: false,
|
||||
importOrderSortSpecifiers: true,
|
||||
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
||||
};
|
||||
// Keeping this file to prevent applying the global monorepo config for now
|
||||
module.exports = {};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
- 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,18 +91,21 @@ const fontSize = ref("2rem");
|
||||
|
||||
This project is using Font Awesome 6 Free.
|
||||
|
||||
Here is how to use an icon in your template.
|
||||
Icons can be displayed with the `UiIcon` component.
|
||||
|
||||
Note: `FontAwesomeIcon` is a global component that does not need to be imported.
|
||||
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.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<FontAwesomeIcon :icon="faDisplay" />
|
||||
<UiIcon :icon="faDisplay" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
</script>
|
||||
```
|
||||
@@ -115,8 +118,6 @@ Here is the equivalent between font weight and style name.
|
||||
| ---------- | ----------- |
|
||||
| Solid | 900 |
|
||||
| Regular | 400 |
|
||||
| Light | 300 |
|
||||
| Thin | 100 |
|
||||
|
||||
### CSS
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"@novnc/novnc": "^1.3.0",
|
||||
"@types/d3-time-format": "^4.0.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"@vueuse/core": "^9.5.0",
|
||||
"@vueuse/math": "^9.5.0",
|
||||
"complex-matcher": "^0.7.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
@@ -40,18 +41,18 @@
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||
"@limegrass/eslint-plugin-import-alias": "^1.0.5",
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
|
||||
"@types/node": "^16.11.41",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@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",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-nested": "^5.0.6",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^2.9.12",
|
||||
"vue-tsc": "^0.38.1"
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-nested": "^6.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.4",
|
||||
"vue-tsc": "^1.0.9"
|
||||
},
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/lite",
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
</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";
|
||||
@@ -58,13 +61,28 @@ 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill" />
|
||||
</div>
|
||||
<div class="badge" v-if="label !== undefined">
|
||||
<div class="legend" v-if="label !== undefined">
|
||||
<span class="circle" />
|
||||
{{ label }}
|
||||
<UiBadge>{{ badgeLabel ?? progressWithUnit }}</UiBadge>
|
||||
<UiBadge class="badge">{{ badgeLabel ?? progressWithUnit }}</UiBadge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,9 +33,14 @@ const progressWithUnit = computed(() => {
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.badge {
|
||||
.legend {
|
||||
text-align: right;
|
||||
margin: 1rem 0;
|
||||
margin: 1.6em 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.9em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.circle {
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
<template>
|
||||
<div v-if="data.length !== 0">
|
||||
<div>
|
||||
<div class="header">
|
||||
<slot name="header" />
|
||||
</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>
|
||||
<template v-if="data !== undefined">
|
||||
<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>
|
||||
</template>
|
||||
<UiSpinner v-else class="spinner" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
|
||||
interface Data {
|
||||
id: string;
|
||||
@@ -29,7 +33,7 @@ interface Data {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: Array<Data>;
|
||||
data?: Array<Data>;
|
||||
nItems?: number;
|
||||
}
|
||||
|
||||
@@ -40,7 +44,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 {
|
||||
@@ -72,6 +76,14 @@ const computedData = computed(() => {
|
||||
font-size: 14px;
|
||||
color: var(--color-blue-scale-300);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
color: var(--color-extra-blue-base);
|
||||
display: flex;
|
||||
margin: auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<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,6 +14,7 @@
|
||||
:label="$t('vms')"
|
||||
/>
|
||||
</template>
|
||||
<UiSpinner v-else class="spinner" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
@@ -22,6 +23,7 @@ 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";
|
||||
@@ -45,3 +47,13 @@ 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="data.result" :nItems="5">
|
||||
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="N_ITEMS">
|
||||
<template #header>
|
||||
<span>{{ $t("storage") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
</template>
|
||||
<template #footer v-if="showFooter">
|
||||
<div class="footer-card">
|
||||
@@ -37,6 +37,7 @@ 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="data" :n-items="5">
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
|
||||
<template #header>
|
||||
<span>{{ $t("hosts") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@ 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",
|
||||
@@ -42,4 +43,10 @@ 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="data" :n-items="5">
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
|
||||
<template #header>
|
||||
<span>{{ $t("vms") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
|
||||
</template>
|
||||
</UsageBar>
|
||||
</template>
|
||||
@@ -13,6 +13,7 @@ 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",
|
||||
@@ -42,4 +43,10 @@ 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>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<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>
|
||||
@@ -0,0 +1,52 @@
|
||||
<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>
|
||||
@@ -22,7 +22,7 @@ defineProps<{
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
padding: 0 0.8rem;
|
||||
height: 2.4rem;
|
||||
height: 1.8em;
|
||||
color: var(--color-blue-scale-500);
|
||||
border-radius: 9.6rem;
|
||||
background-color: var(--color-blue-scale-300);
|
||||
|
||||
@@ -30,7 +30,12 @@ const props = withDefaults(
|
||||
transparent?: boolean;
|
||||
active?: boolean;
|
||||
}>(),
|
||||
{ busy: undefined, disabled: undefined, outlined: undefined }
|
||||
{
|
||||
busy: undefined,
|
||||
disabled: undefined,
|
||||
outlined: undefined,
|
||||
transparent: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const isGroupBusy = inject("isButtonGroupBusy", false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="ui-button-group">
|
||||
<div :class="{ merge }" class="ui-button-group">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,6 +14,7 @@ const props = defineProps<{
|
||||
color?: Color;
|
||||
outlined?: boolean;
|
||||
transparent?: boolean;
|
||||
merge?: boolean;
|
||||
}>();
|
||||
provide(
|
||||
"isButtonGroupBusy",
|
||||
@@ -40,8 +41,32 @@ provide(
|
||||
<style lang="postcss" scoped>
|
||||
.ui-button-group {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
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>
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
# 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>
|
||||
```
|
||||
|
||||
@@ -13,19 +13,23 @@ 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
|
||||
@@ -38,32 +42,32 @@ const { /* ... */ } = useCollectionFilter({ queryStringParam: undefined });
|
||||
@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>
|
||||
```
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
```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,27 +5,28 @@
|
||||
<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,34 +4,30 @@
|
||||
<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';
|
||||
import useMultiSelect from "./multi-select.composable";
|
||||
|
||||
const {
|
||||
selected,
|
||||
areAllSelected,
|
||||
} = useMultiSelect()
|
||||
const { selected, areAllSelected } = useMultiSelect();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -123,3 +123,37 @@ 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,7 +15,9 @@
|
||||
"community-name": "{name} community",
|
||||
"copy": "Copy",
|
||||
"cpu-usage":"CPU usage",
|
||||
"dark-mode": "Dark mode",
|
||||
"theme-dark": "Dark",
|
||||
"theme-light": "Light",
|
||||
"theme-auto": "Auto",
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
"descending": "descending",
|
||||
@@ -37,6 +39,7 @@
|
||||
"or": "Or",
|
||||
"password": "Password",
|
||||
"property": "Property",
|
||||
"ram-usage":"RAM usage",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
"settings": "Settings",
|
||||
"snapshot": "Snapshot",
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"community-name": "Communauté {name}",
|
||||
"copy": "Copier",
|
||||
"cpu-usage":"Utilisation CPU",
|
||||
"dark-mode": "Mode sombre",
|
||||
"theme-dark": "Sombre",
|
||||
"theme-light": "Clair",
|
||||
"theme-auto": "Auto",
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"descending": "descendant",
|
||||
@@ -37,6 +39,7 @@
|
||||
"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é",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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,
|
||||
});
|
||||
@@ -16,7 +15,7 @@ export const useHostStore = defineStore("host", () => {
|
||||
if (host === undefined) {
|
||||
throw new Error(`Host ${id} could not be found.`);
|
||||
}
|
||||
return xapiStats._getAndUpdateStats({
|
||||
return useXenApiStore().getXapiStats()._getAndUpdateStats({
|
||||
host,
|
||||
uuid: host.uuid,
|
||||
granularity,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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,15 +1,14 @@
|
||||
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,
|
||||
@@ -42,7 +41,7 @@ export const useVmStore = defineStore("vm", () => {
|
||||
throw new Error(`VM ${id} is halted or host could not be found.`);
|
||||
}
|
||||
|
||||
return xapiStats._getAndUpdateStats({
|
||||
return useXenApiStore().getXapiStats()._getAndUpdateStats({
|
||||
host,
|
||||
uuid: vm.uuid,
|
||||
granularity,
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
<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,7 +19,9 @@
|
||||
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/"
|
||||
@@ -35,7 +37,9 @@
|
||||
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"
|
||||
@@ -50,14 +54,15 @@
|
||||
<UiKeyValueList>
|
||||
<UiKeyValueRow>
|
||||
<template #key>{{ $t("appearance") }}</template>
|
||||
<template #value
|
||||
><FormLabel>
|
||||
<FormToggle
|
||||
:modelValue="darkMode"
|
||||
@update:modelValue="setDarkMode"
|
||||
/>{{ $t("dark-mode") }}</FormLabel
|
||||
></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>
|
||||
</UiKeyValueRow>
|
||||
</UiKeyValueList>
|
||||
</UiCard>
|
||||
@@ -85,15 +90,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { 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";
|
||||
@@ -105,12 +111,7 @@ const { locale } = useI18n();
|
||||
|
||||
watch(locale, (newLocale) => localStorage.setItem("lang", newLocale));
|
||||
|
||||
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");
|
||||
};
|
||||
const { colorMode } = storeToRefs(useUiStore());
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["ES2019"],
|
||||
"lib": [
|
||||
"ES2019",
|
||||
"dom"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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),
|
||||
|
||||
33
@xen-orchestra/log/_compileGlobPattern.js
Normal file
33
@xen-orchestra/log/_compileGlobPattern.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'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,5 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const compileGlobPattern = require('./_compileGlobPattern.js')
|
||||
const createTransport = require('./transports/console')
|
||||
const { LEVELS, resolve } = require('./levels')
|
||||
|
||||
@@ -8,8 +9,19 @@ 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 level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
|
||||
global[symbol] = log => log.level >= level && transport(log)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const fromCallback = require('promise-toolbox/fromCallback')
|
||||
// eslint-disable-next-line n/no-missing-require
|
||||
// eslint-disable-next-line n/no-extraneous-require
|
||||
const splitHost = require('split-host')
|
||||
// eslint-disable-next-line n/no-missing-require
|
||||
// eslint-disable-next-line n/no-extraneous-require
|
||||
const { createClient, Facility, Severity, Transport } = require('syslog-client')
|
||||
|
||||
const LEVELS = require('../levels')
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
'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]
|
||||
@@ -14,39 +10,6 @@ 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('./utils')
|
||||
const compileGlobPattern = require('./_compileGlobPattern.js')
|
||||
|
||||
describe('compileGlobPattern()', () => {
|
||||
it('works', () => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
- 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,6 +12,29 @@ 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
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,9 +1,19 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.76.1** (2022-11-08)
|
||||
## **5.76.2** (2022-11-14)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### 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
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [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))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> 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
|
||||
- [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
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -30,6 +34,13 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @xen-orchestra/backups-cli major
|
||||
- @xen-orchestra/fs minor
|
||||
- @xen-orchestra/log minor
|
||||
- vhd-lib minor
|
||||
- xo-cli patch
|
||||
- xo-server minor
|
||||
- xo-vmdk-to-vhd minor
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
21
package.json
21
package.json
@@ -20,7 +20,7 @@
|
||||
"getopts": "^2.3.0",
|
||||
"globby": "^13.1.1",
|
||||
"handlebars": "^4.7.6",
|
||||
"husky": "^4.2.5",
|
||||
"husky": "^8.0.2",
|
||||
"jest": "^29.0.3",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lodash": "^4.17.4",
|
||||
@@ -34,11 +34,6 @@
|
||||
"node": ">=14",
|
||||
"yarn": "^1.7.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged && scripts/lint-staged.js"
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
"^(@vates/[^/]+)$": [
|
||||
@@ -75,21 +70,29 @@
|
||||
"testRegex": "\\.spec\\.js$"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{md,ts,ts}": "prettier --write"
|
||||
"*": [
|
||||
"scripts/run-changed-pkgs.js test",
|
||||
"prettier --ignore-unknown --write"
|
||||
],
|
||||
"*.{{,c,m}j,t}s{,x}": [
|
||||
"eslint --ignore-pattern '!*'",
|
||||
"jest --testRegex='^(?!.*.integ.spec.js$).*.spec.js$' --findRelatedTests --passWithNoTests"
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "scripts/run-script.js --parallel --concurrency 2 build",
|
||||
"ci": "yarn && yarn build && yarn test-integration",
|
||||
"ci": "yarn && scripts/run-script.js --parallel prepare && yarn test-lint && yarn test-integration",
|
||||
"clean": "scripts/run-script.js --parallel clean",
|
||||
"dev": "scripts/run-script.js --parallel dev",
|
||||
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
|
||||
"docs:dev": "vuepress dev docs",
|
||||
"docs:build": "vuepress build docs",
|
||||
"prepare": "husky install",
|
||||
"prettify": "prettier --ignore-path .gitignore --write '**/*.{cjs,js,jsx,md,mjs,ts,tsx}'",
|
||||
"test": "npm run test-lint && npm run test-unit",
|
||||
"test-integration": "jest \".integ\\.spec\\.js$\"",
|
||||
"test-lint": "eslint --ignore-path .gitignore .",
|
||||
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
|
||||
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js test",
|
||||
"travis-tests": "scripts/travis-tests.js"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('assert').strict
|
||||
|
||||
const { ast, pattern } = require('./index.fixtures')
|
||||
const {
|
||||
getPropertyClausesStrings,
|
||||
@@ -17,7 +18,7 @@ const {
|
||||
|
||||
it('getPropertyClausesStrings', () => {
|
||||
const tmp = getPropertyClausesStrings(parse('foo bar:baz baz:|(foo bar /^boo$/ /^far$/) foo:/^bar$/'))
|
||||
expect(tmp).toEqual({
|
||||
assert.deepEqual(tmp, {
|
||||
bar: ['baz'],
|
||||
baz: ['foo', 'bar', 'boo', 'far'],
|
||||
foo: ['bar'],
|
||||
@@ -26,72 +27,72 @@ it('getPropertyClausesStrings', () => {
|
||||
|
||||
describe('parse', () => {
|
||||
it('analyses a string and returns a node/tree', () => {
|
||||
expect(parse(pattern)).toEqual(ast)
|
||||
assert.deepEqual(parse(pattern), ast)
|
||||
})
|
||||
|
||||
it('supports an empty string', () => {
|
||||
expect(parse('')).toEqual(new Null())
|
||||
assert.deepEqual(parse(''), new Null())
|
||||
})
|
||||
|
||||
it('differentiate between numbers and numbers in strings', () => {
|
||||
let node
|
||||
|
||||
node = parse('32')
|
||||
expect(node.match(32)).toBe(true)
|
||||
expect(node.match('32')).toBe(true)
|
||||
expect(node.toString()).toBe('32')
|
||||
assert.equal(node.match(32), true)
|
||||
assert.equal(node.match('32'), true)
|
||||
assert.equal(node.toString(), '32')
|
||||
|
||||
node = parse('"32"')
|
||||
expect(node.match(32)).toBe(false)
|
||||
expect(node.match('32')).toBe(true)
|
||||
expect(node.toString()).toBe('"32"')
|
||||
assert.equal(node.match(32), false)
|
||||
assert.equal(node.match('32'), true)
|
||||
assert.equal(node.toString(), '"32"')
|
||||
})
|
||||
|
||||
it('supports non-ASCII letters in raw strings', () => {
|
||||
expect(parse('åäöé:ÅÄÖÉ')).toStrictEqual(new Property('åäöé', new StringNode('ÅÄÖÉ')))
|
||||
assert.deepEqual(parse('åäöé:ÅÄÖÉ'), new Property('åäöé', new StringNode('ÅÄÖÉ')))
|
||||
})
|
||||
})
|
||||
|
||||
describe('GlobPattern', () => {
|
||||
it('matches a glob pattern recursively', () => {
|
||||
expect(new GlobPattern('b*r').match({ foo: 'bar' })).toBe(true)
|
||||
assert.equal(new GlobPattern('b*r').match({ foo: 'bar' }), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Number', () => {
|
||||
it('match a number recursively', () => {
|
||||
expect(new NumberNode(3).match([{ foo: 3 }])).toBe(true)
|
||||
assert.equal(new NumberNode(3).match([{ foo: 3 }]), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('NumberOrStringNode', () => {
|
||||
it('match a string', () => {
|
||||
expect(new NumberOrStringNode('123').match([{ foo: '123' }])).toBe(true)
|
||||
assert.equal(new NumberOrStringNode('123').match([{ foo: '123' }]), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setPropertyClause', () => {
|
||||
it('creates a node if none passed', () => {
|
||||
expect(setPropertyClause(undefined, 'foo', 'bar').toString()).toBe('foo:bar')
|
||||
assert.equal(setPropertyClause(undefined, 'foo', 'bar').toString(), 'foo:bar')
|
||||
})
|
||||
|
||||
it('adds a property clause if there was none', () => {
|
||||
expect(setPropertyClause(parse('baz'), 'foo', 'bar').toString()).toBe('baz foo:bar')
|
||||
assert.equal(setPropertyClause(parse('baz'), 'foo', 'bar').toString(), 'baz foo:bar')
|
||||
})
|
||||
|
||||
it('replaces the property clause if there was one', () => {
|
||||
expect(setPropertyClause(parse('plip foo:baz plop'), 'foo', 'bar').toString()).toBe('plip plop foo:bar')
|
||||
assert.equal(setPropertyClause(parse('plip foo:baz plop'), 'foo', 'bar').toString(), 'plip plop foo:bar')
|
||||
|
||||
expect(setPropertyClause(parse('foo:|(baz plop)'), 'foo', 'bar').toString()).toBe('foo:bar')
|
||||
assert.equal(setPropertyClause(parse('foo:|(baz plop)'), 'foo', 'bar').toString(), 'foo:bar')
|
||||
})
|
||||
|
||||
it('removes the property clause if no chid is passed', () => {
|
||||
expect(setPropertyClause(parse('foo bar:baz qux'), 'bar', undefined).toString()).toBe('foo qux')
|
||||
assert.equal(setPropertyClause(parse('foo bar:baz qux'), 'bar', undefined).toString(), 'foo qux')
|
||||
|
||||
expect(setPropertyClause(parse('foo bar:baz qux'), 'baz', undefined).toString()).toBe('foo bar:baz qux')
|
||||
assert.equal(setPropertyClause(parse('foo bar:baz qux'), 'baz', undefined).toString(), 'foo bar:baz qux')
|
||||
})
|
||||
})
|
||||
|
||||
it('toString', () => {
|
||||
expect(ast.toString()).toBe(pattern)
|
||||
assert.equal(ast.toString(), pattern)
|
||||
})
|
||||
@@ -26,6 +26,10 @@
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// This file has been generated by [index-modules](https://npmjs.com/index-modules)
|
||||
//
|
||||
|
||||
var d = Object.defineProperty
|
||||
'use strict'
|
||||
|
||||
const d = Object.defineProperty
|
||||
function de(o, n, v) {
|
||||
d(o, n, { enumerable: true, value: v })
|
||||
return v
|
||||
@@ -17,7 +19,7 @@ function dl(o, n, g, a) {
|
||||
})
|
||||
}
|
||||
function r(p) {
|
||||
var v = require(p)
|
||||
const v = require(p)
|
||||
return v && v.__esModule
|
||||
? v
|
||||
: typeof v === 'object' || typeof v === 'function'
|
||||
@@ -32,7 +34,7 @@ function e(p, i) {
|
||||
}
|
||||
|
||||
d(exports, '__esModule', { value: true })
|
||||
var defaults = de(exports, 'default', {})
|
||||
const defaults = de(exports, 'default', {})
|
||||
e('./check.js', 'check')
|
||||
e('./compare.js', 'compare')
|
||||
e('./copy.js', 'copy')
|
||||
|
||||
@@ -95,15 +95,9 @@ test('It rename and unlink a VHDFile', async () => {
|
||||
await convertFromRawToVhd(rawFileName, vhdFileName)
|
||||
await Disposable.use(async function* () {
|
||||
const handler = yield getSyncedHandler({ url: 'file:///' })
|
||||
const { size } = await fs.stat(vhdFileName)
|
||||
const targetFileName = `${tempDir}/renamed.vhd`
|
||||
|
||||
await VhdAbstract.rename(handler, vhdFileName, targetFileName)
|
||||
await VhdAbstract.unlink(handler, vhdFileName)
|
||||
expect(await fs.exists(vhdFileName)).toEqual(false)
|
||||
const { size: renamedSize } = await fs.stat(targetFileName)
|
||||
expect(size).toEqual(renamedSize)
|
||||
await VhdAbstract.unlink(handler, targetFileName)
|
||||
expect(await fs.exists(targetFileName)).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -122,12 +116,8 @@ test('It rename and unlink a VhdDirectory', async () => {
|
||||
// it should clean an existing directory
|
||||
await fs.mkdir(targetFileName)
|
||||
await fs.writeFile(`${targetFileName}/dummy`, 'I exists')
|
||||
await VhdAbstract.rename(handler, vhdDirectory, targetFileName)
|
||||
expect(await fs.exists(vhdDirectory)).toEqual(false)
|
||||
expect(await fs.exists(targetFileName)).toEqual(true)
|
||||
await VhdAbstract.unlink(handler, `${targetFileName}/dummy`)
|
||||
expect(await fs.exists(`${targetFileName}/dummy`)).toEqual(false)
|
||||
await VhdAbstract.unlink(handler, targetFileName)
|
||||
expect(await fs.exists(targetFileName)).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -138,7 +128,6 @@ test('It create , rename and unlink alias', async () => {
|
||||
const vhdFileName = `${tempDir}/randomfile.vhd`
|
||||
await convertFromRawToVhd(rawFileName, vhdFileName)
|
||||
const aliasFileName = `${tempDir}/aliasFileName.alias.vhd`
|
||||
const aliasFileNameRenamed = `${tempDir}/aliasFileNameRenamed.alias.vhd`
|
||||
|
||||
await Disposable.use(async function* () {
|
||||
const handler = yield getSyncedHandler({ url: 'file:///' })
|
||||
@@ -146,15 +135,9 @@ test('It create , rename and unlink alias', async () => {
|
||||
expect(await fs.exists(aliasFileName)).toEqual(true)
|
||||
expect(await fs.exists(vhdFileName)).toEqual(true)
|
||||
|
||||
await VhdAbstract.rename(handler, aliasFileName, aliasFileNameRenamed)
|
||||
expect(await fs.exists(aliasFileName)).toEqual(false)
|
||||
expect(await fs.exists(vhdFileName)).toEqual(true)
|
||||
expect(await fs.exists(aliasFileNameRenamed)).toEqual(true)
|
||||
|
||||
await VhdAbstract.unlink(handler, aliasFileNameRenamed)
|
||||
await VhdAbstract.unlink(handler, aliasFileName)
|
||||
expect(await fs.exists(aliasFileName)).toEqual(false)
|
||||
expect(await fs.exists(vhdFileName)).toEqual(false)
|
||||
expect(await fs.exists(aliasFileNameRenamed)).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -200,14 +200,6 @@ exports.VhdAbstract = class VhdAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
static async rename(handler, sourcePath, targetPath) {
|
||||
try {
|
||||
// delete target if it already exists
|
||||
await VhdAbstract.unlink(handler, targetPath)
|
||||
} catch (e) {}
|
||||
await handler.rename(sourcePath, targetPath)
|
||||
}
|
||||
|
||||
static async unlink(handler, path) {
|
||||
const resolved = await resolveVhdAlias(handler, path)
|
||||
try {
|
||||
|
||||
@@ -43,7 +43,7 @@ const { warn } = createLogger('vhd-lib:merge')
|
||||
|
||||
// write the merge progress file at most every `delay` seconds
|
||||
function makeThrottledWriter(handler, path, delay) {
|
||||
let lastWrite = Date.now()
|
||||
let lastWrite = 0
|
||||
return async json => {
|
||||
const now = Date.now()
|
||||
if (now - lastWrite > delay) {
|
||||
@@ -61,7 +61,7 @@ async function cleanupVhds(handler, chain, { logInfo = noop, removeUnused = fals
|
||||
const children = chain.slice(1, -1)
|
||||
const mergeTargetChild = chain[chain.length - 1]
|
||||
|
||||
await VhdAbstract.rename(handler, parent, mergeTargetChild)
|
||||
await handler.rename(parent, mergeTargetChild)
|
||||
|
||||
return asyncMap(children, child => {
|
||||
logInfo(`the VHD child is already merged`, { child })
|
||||
@@ -175,6 +175,7 @@ module.exports.mergeVhdChain = limitConcurrency(2)(async function mergeVhdChain(
|
||||
let counter = 0
|
||||
|
||||
const mergeStateWriter = makeThrottledWriter(handler, mergeStatePath, 10e3)
|
||||
await mergeStateWriter(mergeState)
|
||||
await asyncEach(
|
||||
toMerge,
|
||||
async blockId => {
|
||||
|
||||
5
packages/xen-api/examples/.eslintrc.js
Normal file
5
packages/xen-api/examples/.eslintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
ignorePatterns: ['*'],
|
||||
}
|
||||
@@ -273,7 +273,9 @@ async function main(args) {
|
||||
const lines = [error.message]
|
||||
const { errors } = error.data
|
||||
errors.forEach(error => {
|
||||
lines.push(` property ${error.property}: ${error.message}`)
|
||||
let { instancePath } = error
|
||||
instancePath = instancePath.length === 0 ? '@' : '@.' + instancePath
|
||||
lines.push(` property ${instancePath}: ${error.message}`)
|
||||
})
|
||||
throw lines.join('\n')
|
||||
})
|
||||
|
||||
@@ -55,3 +55,4 @@ setTimeout(function () {
|
||||
name: 'Steve',
|
||||
})
|
||||
}, 10)
|
||||
/* eslint-enable no-console */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
'use strict'
|
||||
|
||||
process.on('unhandledRejection', function (error) {
|
||||
@@ -59,3 +61,5 @@ xo.open()
|
||||
.then(function () {
|
||||
return xo.close()
|
||||
})
|
||||
|
||||
/* eslint-enable no-console */
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* eslint-env jest */
|
||||
'use strict'
|
||||
|
||||
import deepFreeze from 'deep-freeze'
|
||||
const { describe, it } = require('test')
|
||||
const { strict: assert } = require('assert')
|
||||
|
||||
import { parse, format } from './'
|
||||
const deepFreeze = require('deep-freeze')
|
||||
|
||||
const { parse, format } = require('./')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -132,6 +135,7 @@ const parseData = deepFreeze({
|
||||
object: {
|
||||
type: 'nfs',
|
||||
host: '192.168.100.225',
|
||||
port: undefined,
|
||||
path: '/media/nfs',
|
||||
},
|
||||
},
|
||||
@@ -203,7 +207,7 @@ describe('format', () => {
|
||||
for (const name in formatData) {
|
||||
const datum = formatData[name]
|
||||
it(name, () => {
|
||||
expect(format(datum.object)).toBe(datum.string)
|
||||
assert.equal(format(datum.object), datum.string)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -212,7 +216,7 @@ describe('parse', () => {
|
||||
for (const name in parseData) {
|
||||
const datum = parseData[name]
|
||||
it(name, () => {
|
||||
expect(parse(datum.string)).toEqual(datum.object)
|
||||
assert.deepEqual(parse(datum.string), datum.object)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -31,7 +31,8 @@
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"rimraf": "^3.0.0"
|
||||
"rimraf": "^3.0.0",
|
||||
"test": "^3.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
@@ -39,6 +40,7 @@
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
'use strict'
|
||||
|
||||
// This is one of the simplest xo-server's plugin than can be created.
|
||||
@@ -78,3 +80,5 @@ exports.default = function (opts) {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable no-console */
|
||||
|
||||
@@ -6,6 +6,7 @@ function handleHook(type, data) {
|
||||
const hooks = this._hooks[data.method]?.[type]
|
||||
if (hooks !== undefined) {
|
||||
return Promise.all(
|
||||
// eslint-disable-next-line array-callback-return
|
||||
hooks.map(({ url, waitForResponse = false }) => {
|
||||
const promise = this._makeRequest(url, type, data).catch(error => {
|
||||
log.error('web hook failed', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.106.0",
|
||||
"version": "5.106.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -25,6 +25,7 @@ register.params = {
|
||||
},
|
||||
authenticationToken: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
register.resolve = {
|
||||
|
||||
@@ -57,8 +57,10 @@ export async function copyVm({ vm, sr }) {
|
||||
|
||||
// full
|
||||
{
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('export full VM...')
|
||||
const input = await srcXapi.VM_export(vm._xapiRef)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('import full VM...')
|
||||
await tgtXapi.VM_destroy((await tgtXapi.importVm(input, { srId: sr })).$ref)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createLogger } from '@xen-orchestra/log'
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
import { defer } from 'golike-defer'
|
||||
import { format, parse } from 'json-rpc-peer'
|
||||
import { incorrectState, noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { incorrectState, invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { parseDuration } from '@vates/parse-duration'
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
import { Ref } from 'xen-api'
|
||||
@@ -106,6 +106,14 @@ export default class Proxy {
|
||||
|
||||
@synchronizedWrite
|
||||
async registerProxy({ address, authenticationToken, name = this._generateDefaultProxyName(), vmUuid }) {
|
||||
if (address === undefined && vmUuid === undefined) {
|
||||
throw invalidParameters('at least one of address and vmUuid must be defined')
|
||||
}
|
||||
|
||||
if (authenticationToken === undefined) {
|
||||
authenticationToken = await generateToken()
|
||||
}
|
||||
|
||||
await this._throwIfRegistered(address, vmUuid)
|
||||
|
||||
const { id } = await this._db.add({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
|
||||
import { basename } from 'path'
|
||||
import { format, parse } from 'xo-remote-parser'
|
||||
import {
|
||||
DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
UNENCRYPTED_ALGORITHM,
|
||||
} from '@xen-orchestra/fs'
|
||||
import { ignoreErrors, timeout } from 'promise-toolbox'
|
||||
import { noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { synchronized } from 'decorator-synchronized'
|
||||
|
||||
import * as sensitiveValues from '../sensitive-values.mjs'
|
||||
@@ -22,6 +23,13 @@ const obfuscateRemote = ({ url, ...remote }) => {
|
||||
return remote
|
||||
}
|
||||
|
||||
function validatePath(url) {
|
||||
const { path } = parse(url)
|
||||
if (path !== undefined && basename(path) === 'xo-vm-backups') {
|
||||
throw invalidParameters('remote url should not end with xo-vm-backups')
|
||||
}
|
||||
}
|
||||
|
||||
export default class {
|
||||
constructor(app) {
|
||||
this._handlers = { __proto__: null }
|
||||
@@ -191,6 +199,8 @@ export default class {
|
||||
}
|
||||
|
||||
async createRemote({ name, options, proxy, url }) {
|
||||
validatePath(url)
|
||||
|
||||
const params = {
|
||||
enabled: false,
|
||||
error: '',
|
||||
@@ -224,6 +234,10 @@ export default class {
|
||||
|
||||
@synchronized()
|
||||
async _updateRemote(id, { url, ...props }) {
|
||||
if (url !== undefined) {
|
||||
validatePath(url)
|
||||
}
|
||||
|
||||
const remote = await this._getRemote(id)
|
||||
|
||||
// url is handled separately to take care of obfuscated values
|
||||
|
||||
@@ -29,7 +29,8 @@ async function vmdkToVhd(vmdkReadStream, grainLogicalAddressList, grainFileOffse
|
||||
|
||||
export async function computeVmdkLength(diskName, vhdReadStream) {
|
||||
let length = 0
|
||||
for await (const b of await vhdToVMDKIterator(diskName, vhdReadStream)) {
|
||||
const { iterator } = await vhdToVMDKIterator(diskName, vhdReadStream)
|
||||
for await (const b of iterator) {
|
||||
length += b.length
|
||||
}
|
||||
return length
|
||||
@@ -43,13 +44,15 @@ export async function computeVmdkLength(diskName, vhdReadStream) {
|
||||
* @returns a readable stream representing a VMDK file
|
||||
*/
|
||||
export async function vhdToVMDK(diskName, vhdReadStreamGetter, withLength = false) {
|
||||
const { iterator, size } = await vhdToVMDKIterator(diskName, await vhdReadStreamGetter())
|
||||
let length
|
||||
const stream = await asyncIteratorToStream(iterator)
|
||||
if (withLength) {
|
||||
length = await computeVmdkLength(diskName, await vhdReadStreamGetter())
|
||||
}
|
||||
const iterable = await vhdToVMDKIterator(diskName, await vhdReadStreamGetter())
|
||||
const stream = await asyncIteratorToStream(iterable)
|
||||
if (withLength) {
|
||||
if (size === undefined) {
|
||||
length = await computeVmdkLength(diskName, await vhdReadStreamGetter())
|
||||
} else {
|
||||
length = size
|
||||
}
|
||||
stream.length = length
|
||||
}
|
||||
return stream
|
||||
@@ -62,8 +65,15 @@ export async function vhdToVMDK(diskName, vhdReadStreamGetter, withLength = fals
|
||||
* @returns a readable stream representing a VMDK file
|
||||
*/
|
||||
export async function vhdToVMDKIterator(diskName, vhdReadStream) {
|
||||
const { blockSize, blocks, diskSize, geometry } = await parseVhdToBlocks(vhdReadStream)
|
||||
return generateVmdkData(diskName, diskSize, blockSize, blocks, geometry)
|
||||
const { blockSize, blockCount, blocks, diskSize, geometry } = await parseVhdToBlocks(vhdReadStream)
|
||||
|
||||
const vmdkTargetSize = blockSize * blockCount + 3 * 1024 * 1024 // header/footer/descriptor
|
||||
const iterator = await generateVmdkData(diskName, diskSize, blockSize, blocks, geometry, vmdkTargetSize)
|
||||
|
||||
return {
|
||||
iterator,
|
||||
size: vmdkTargetSize,
|
||||
}
|
||||
}
|
||||
|
||||
export { ParsableFile, parseOVAFile, vmdkToVhd, writeOvaOn }
|
||||
|
||||
@@ -32,17 +32,18 @@ export async function writeOvaOn(
|
||||
|
||||
// https://github.com/mafintosh/tar-stream/issues/24#issuecomment-558358268
|
||||
async function pushDisk(disk) {
|
||||
const size = await computeVmdkLength(disk.name, await disk.getStream())
|
||||
let { iterator, size } = await vhdToVMDKIterator(disk.name, await disk.getStream())
|
||||
if (size === undefined) {
|
||||
size = await computeVmdkLength(disk.name, await disk.getStream())
|
||||
}
|
||||
disk.fileSize = size
|
||||
const blockIterator = await vhdToVMDKIterator(disk.name, await disk.getStream())
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const entry = pack.entry({ name: `${disk.name}.vmdk`, size: size }, err => {
|
||||
const entry = pack.entry({ name: `${disk.name}.vmdk`, size }, err => {
|
||||
if (err == null) {
|
||||
return resolve()
|
||||
} else return reject(err)
|
||||
})
|
||||
return writeDisk(entry, blockIterator).then(
|
||||
return writeDisk(entry, iterator).then(
|
||||
() => entry.end(),
|
||||
e => reject(e)
|
||||
)
|
||||
|
||||
@@ -33,7 +33,8 @@ export async function generateVmdkData(
|
||||
sectorsPerTrackCylinder: 63,
|
||||
heads: 16,
|
||||
cylinders: 10402,
|
||||
}
|
||||
},
|
||||
targetSize
|
||||
) {
|
||||
const cid = Math.floor(Math.random() * Math.pow(2, 32))
|
||||
const diskCapacitySectors = Math.ceil(diskCapacityBytes / SECTOR_SIZE)
|
||||
@@ -150,10 +151,39 @@ ddb.geometry.cylinders = "${geometry.cylinders}"
|
||||
}
|
||||
}
|
||||
|
||||
function* padding() {
|
||||
if (targetSize === undefined) {
|
||||
return
|
||||
}
|
||||
let remaining = targetSize - streamPosition
|
||||
remaining -= SECTOR_SIZE // MARKER_GT
|
||||
remaining -= tableBuffer.length
|
||||
remaining -= SECTOR_SIZE // MARKER_GD
|
||||
remaining -= roundToSector(headerData.grainDirectoryEntries * 4)
|
||||
remaining -= SECTOR_SIZE // MARKER_GT
|
||||
remaining -= tableBuffer.length
|
||||
remaining -= SECTOR_SIZE // MARKER_GD
|
||||
remaining -= roundToSector(headerData.grainDirectoryEntries * 4)
|
||||
remaining -= SECTOR_SIZE // MARKER_FOOTER
|
||||
remaining -= SECTOR_SIZE // stream optimizedheader
|
||||
remaining -= SECTOR_SIZE // MARKER_EOS
|
||||
|
||||
if (remaining < 0) {
|
||||
throw new Error('vmdk is bigger than precalculed size ')
|
||||
}
|
||||
const size = 1024 * 1024
|
||||
while (remaining > 0) {
|
||||
const yieldSize = Math.min(size, remaining)
|
||||
remaining -= yieldSize
|
||||
yield track(Buffer.alloc(yieldSize))
|
||||
}
|
||||
}
|
||||
|
||||
async function* iterator() {
|
||||
yield track(headerData.buffer)
|
||||
yield track(descriptorBuffer)
|
||||
yield* emitBlocks(grainSizeBytes, blockGenerator)
|
||||
yield* padding()
|
||||
yield track(createEmptyMarker(MARKER_GT))
|
||||
let tableOffset = streamPosition
|
||||
// grain tables
|
||||
@@ -181,6 +211,5 @@ ddb.geometry.cylinders = "${geometry.cylinders}"
|
||||
yield track(footer.buffer)
|
||||
yield track(createEmptyMarker(MARKER_EOS))
|
||||
}
|
||||
|
||||
return iterator()
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
homeTemplatePage: 'Шаблоны',
|
||||
|
||||
// Original text: 'Storages'
|
||||
homeSrPage: "Хранилища",
|
||||
homeSrPage: 'Хранилища',
|
||||
|
||||
// Original text: "Dashboard"
|
||||
dashboardPage: 'Контрольные панели',
|
||||
@@ -144,7 +144,7 @@ export default {
|
||||
aboutPage: 'О программе',
|
||||
|
||||
// Original text: 'About XO {xoaPlan}'
|
||||
aboutXoaPlan: "О Xen Orchestra {xoaPlan}",
|
||||
aboutXoaPlan: 'О Xen Orchestra {xoaPlan}',
|
||||
|
||||
// Original text: "New"
|
||||
newMenu: 'Добавить',
|
||||
@@ -399,10 +399,10 @@ export default {
|
||||
highAvailability: 'Высокая доступность',
|
||||
|
||||
// Original text: 'Shared {type}'
|
||||
srSharedType: "Совместное использование {type}",
|
||||
srSharedType: 'Совместное использование {type}',
|
||||
|
||||
// Original text: 'Not shared {type}'
|
||||
srNotSharedType: "Без совместного использования {type}",
|
||||
srNotSharedType: 'Без совместного использования {type}',
|
||||
|
||||
// Original text: "Add"
|
||||
add: 'Добавить',
|
||||
@@ -561,10 +561,10 @@ export default {
|
||||
unknownSchedule: 'Неизвестно',
|
||||
|
||||
// Original text: 'Web browser timezone'
|
||||
timezonePickerUseLocalTime: "Часовой пояс WEB-браузера",
|
||||
timezonePickerUseLocalTime: 'Часовой пояс WEB-браузера',
|
||||
|
||||
// Original text: 'Server timezone ({value})'
|
||||
serverTimezoneOption: "Часовой пояс сервера ({value})",
|
||||
serverTimezoneOption: 'Часовой пояс сервера ({value})',
|
||||
|
||||
// Original text: 'Cron Pattern:'
|
||||
cronPattern: 'Cron-шаблон: ',
|
||||
@@ -726,7 +726,8 @@ export default {
|
||||
localRemoteWarningTitle: undefined,
|
||||
|
||||
// Original text: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.'
|
||||
localRemoteWarningMessage: 'Предупреждение: локальные удаленные устройства будут использовать ограниченное дисковое пространство XOA. Только для продвинутых пользователей.',
|
||||
localRemoteWarningMessage:
|
||||
'Предупреждение: локальные удаленные устройства будут использовать ограниченное дисковое пространство XOA. Только для продвинутых пользователей.',
|
||||
|
||||
// Original text: 'Warning: this feature works only with XenServer 6.5 or newer.'
|
||||
backupVersionWarning: undefined,
|
||||
@@ -2553,7 +2554,8 @@ export default {
|
||||
noHostsAvailable: 'Нет доступных хостов',
|
||||
|
||||
// Original text: "VMs created from this resource set shall run on the following hosts."
|
||||
availableHostsDescription: 'Виртуальные машины, созданные из этого набора ресурсов, должны работать на следующих хостах.',
|
||||
availableHostsDescription:
|
||||
'Виртуальные машины, созданные из этого набора ресурсов, должны работать на следующих хостах.',
|
||||
|
||||
// Original text: "Maximum CPUs"
|
||||
maxCpus: 'Максимум CPUs',
|
||||
@@ -2882,7 +2884,8 @@ export default {
|
||||
deleteVmModalTitle: 'Удалить ВМ',
|
||||
|
||||
// Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
|
||||
deleteVmModalMessage: 'Вы уверены, что хотите удалить эту виртуальную машину? ВСЕ ДИСКИ ВИРТУАЛЬНОЙ МАШИНЫ БУДУТ УДАЛЕНЫ!',
|
||||
deleteVmModalMessage:
|
||||
'Вы уверены, что хотите удалить эту виртуальную машину? ВСЕ ДИСКИ ВИРТУАЛЬНОЙ МАШИНЫ БУДУТ УДАЛЕНЫ!',
|
||||
|
||||
// Original text: "Migrate VM"
|
||||
migrateVmModalTitle: 'Переместить ВМ',
|
||||
|
||||
@@ -2418,6 +2418,7 @@ const messages = {
|
||||
licensesBinding: 'Licenses binding',
|
||||
notEnoughXcpngLicenses: 'Not enough XCP-ng licenses',
|
||||
notBoundSelectLicense: 'Not bound (Plan (ID), expiration date)',
|
||||
xcpngLicensesBindingAvancedView: "To bind an XCP-ng license, go the pool's Advanced tab.",
|
||||
xosanUnregisteredDisclaimer:
|
||||
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
|
||||
xosanSourcesDisclaimer:
|
||||
|
||||
@@ -591,9 +591,11 @@ export const safeDateFormat = ms => new Date(ms).toISOString().replace(/:/g, '_'
|
||||
// ===================================================================
|
||||
|
||||
export const downloadLog = ({ log, date, type }) => {
|
||||
const isJson = typeof log !== 'string'
|
||||
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = window.URL.createObjectURL(createBlobFromString(log))
|
||||
anchor.download = `${safeDateFormat(date)} - ${type}.log`
|
||||
anchor.href = window.URL.createObjectURL(createBlobFromString(isJson ? JSON.stringify(log, null, 2) : log))
|
||||
anchor.download = `${safeDateFormat(date)} - ${type}.${isJson ? 'json' : 'log'}`
|
||||
anchor.style.display = 'none'
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
|
||||
@@ -373,7 +373,8 @@ export default decorate([
|
||||
const { tasks } = parent
|
||||
if (tasks !== undefined) {
|
||||
for (const task of tasks) {
|
||||
task.parent = parent
|
||||
// parent should not be enumerable as it would create a cycle and break JSON.stringify
|
||||
Object.defineProperty(task, parent, { value: parent })
|
||||
linkParent(task)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ export default decorate([
|
||||
effects: {
|
||||
_downloadLog:
|
||||
() =>
|
||||
({ formattedLog }, { log }) =>
|
||||
downloadLog({ log: formattedLog, date: log.start, type: 'backup NG' }),
|
||||
(_, { log }) =>
|
||||
downloadLog({ log, date: log.start, type: 'backup NG' }),
|
||||
restartFailedVms:
|
||||
(_, params) =>
|
||||
async (_, { log: { jobId: id, scheduleId: schedule, tasks, infos } }) => {
|
||||
|
||||
@@ -94,7 +94,7 @@ const INDIVIDUAL_ACTIONS = [
|
||||
label: _('logDownload'),
|
||||
handler: task =>
|
||||
downloadLog({
|
||||
log: JSON.stringify(task, null, 2),
|
||||
log: task,
|
||||
date: task.start,
|
||||
type: 'Metadata restore',
|
||||
}),
|
||||
|
||||
@@ -323,6 +323,9 @@ export default class Licenses extends Component {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row className='text-info mb-1'>
|
||||
<Icon icon='info' /> <i>{_('xcpngLicensesBindingAvancedView')}</i>
|
||||
</Row>
|
||||
<Row className='mb-1'>
|
||||
<Col>
|
||||
<a
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
const formatFiles = files => {
|
||||
run('./node_modules/.bin/prettier', ['--write'].concat(files))
|
||||
}
|
||||
const testFiles = files => {
|
||||
run('./node_modules/.bin/eslint', ['--ignore-pattern', '!*'].concat(files))
|
||||
run(
|
||||
'./node_modules/.bin/jest',
|
||||
['--testRegex=^(?!.*.integ.spec.js$).*.spec.js$', '--findRelatedTests', '--passWithNoTests'].concat(files)
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const { execFileSync, spawnSync } = require('child_process')
|
||||
const { readFileSync, writeFileSync } = require('fs')
|
||||
|
||||
const run = (command, args) => {
|
||||
const { status } = spawnSync(command, args, { stdio: 'inherit' })
|
||||
if (status !== 0) {
|
||||
process.exit(status)
|
||||
}
|
||||
}
|
||||
|
||||
const gitDiff = (what, args = []) =>
|
||||
execFileSync('git', ['diff-' + what, '--diff-filter=AM', '--ignore-submodules', '--name-only'].concat(args), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.split('\n')
|
||||
.filter(_ => _ !== '')
|
||||
const gitDiffFiles = (files = []) => gitDiff('files', files)
|
||||
const gitDiffIndex = () => gitDiff('index', ['--cached', 'HEAD'])
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const files = gitDiffIndex().filter(_ => _.endsWith('.cjs') || _.endsWith('.js') || _.endsWith('.mjs'))
|
||||
if (files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// save the list of files with unstaged changes
|
||||
let unstaged = gitDiffFiles(files)
|
||||
|
||||
// format all files
|
||||
formatFiles(files)
|
||||
|
||||
if (unstaged.length !== 0) {
|
||||
// refresh the list of files with unstaged changes, maybe the
|
||||
// changes have been reverted by the formatting
|
||||
run('git', ['update-index', '-q', '--refresh'])
|
||||
unstaged = gitDiffFiles(unstaged)
|
||||
|
||||
if (unstaged.length !== 0) {
|
||||
const contents = unstaged.map(name => readFileSync(name))
|
||||
process.on('exit', () => unstaged.map((name, i) => writeFileSync(name, contents[i])))
|
||||
run('git', ['checkout'].concat(unstaged))
|
||||
formatFiles(unstaged)
|
||||
}
|
||||
}
|
||||
|
||||
// add formatting changes so that even if the test fails, there won't be
|
||||
// stylistic diffs between files and index
|
||||
run('git', ['add'].concat(files))
|
||||
|
||||
testFiles(files)
|
||||
28
scripts/run-changed-pkgs.js
Executable file
28
scripts/run-changed-pkgs.js
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
const { join, relative, sep } = require('path')
|
||||
|
||||
const [, , script, ...files] = process.argv
|
||||
|
||||
const pkgs = new Set()
|
||||
const root = join(__dirname, '..')
|
||||
for (const file of files) {
|
||||
const parts = relative(root, file).split(sep)
|
||||
if ((parts.length > 2 && parts[0] === 'packages') || parts[0][0] === '@') {
|
||||
pkgs.add(parts.slice(0, 2).join(sep))
|
||||
}
|
||||
}
|
||||
|
||||
if (pkgs.size !== 0) {
|
||||
const args = ['run', '--if-present', script]
|
||||
for (const pkg of pkgs) {
|
||||
args.push('-w', pkg)
|
||||
}
|
||||
|
||||
const { status } = require('child_process').spawnSync('npm', args, { stdio: 'inherit' })
|
||||
if (status !== 0) {
|
||||
process.exit(status)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user