Compare commits
224 Commits
nr-fix-S3-
...
xo-lite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae087a6539 | ||
|
|
4db93f8ced | ||
|
|
e5c737cba7 | ||
|
|
9f0f38ef94 | ||
|
|
d76996b1d5 | ||
|
|
3b77897692 | ||
|
|
d4ed555abd | ||
|
|
97d77c0aa5 | ||
|
|
a9ad0ec455 | ||
|
|
78ec008c26 | ||
|
|
2d71bef5d8 | ||
|
|
3ec7c61987 | ||
|
|
526c2001d3 | ||
|
|
f3d4e40c6d | ||
|
|
ac8f93fb0e | ||
|
|
d2fbc1b573 | ||
|
|
c5670a047f | ||
|
|
e9472889f2 | ||
|
|
9bec4b571c | ||
|
|
b56cc96e37 | ||
|
|
011164f16c | ||
|
|
b9a9471408 | ||
|
|
9abd1429a2 | ||
|
|
7f656973de | ||
|
|
5e0766fcb1 | ||
|
|
2dc5c0e161 | ||
|
|
d0730d05fd | ||
|
|
8fe3a439fc | ||
|
|
12c7113662 | ||
|
|
36be46b073 | ||
|
|
25ef579df5 | ||
|
|
cbbb07d389 | ||
|
|
96df84c9d8 | ||
|
|
17c4b5cbe7 | ||
|
|
cf642cd720 | ||
|
|
047f3a9b4c | ||
|
|
b0f85e0380 | ||
|
|
7aa518b43c | ||
|
|
d187d6aeeb | ||
|
|
289dce3876 | ||
|
|
930afea1a1 | ||
|
|
3801fa9134 | ||
|
|
ae211046b8 | ||
|
|
87ce9ff63a | ||
|
|
131c6321be | ||
|
|
6abcce498f | ||
|
|
9c38f5b327 | ||
|
|
14720d4cbf | ||
|
|
940ef2845d | ||
|
|
e3dbb7a6c2 | ||
|
|
8cba6ebb20 | ||
|
|
a1b322f5be | ||
|
|
07ff19c4b8 | ||
|
|
3a0af4e7e0 | ||
|
|
dbb3f74ab0 | ||
|
|
0eaac8fd7a | ||
|
|
06c71154b9 | ||
|
|
0e8f314dd6 | ||
|
|
f53ec8968b | ||
|
|
919d118f21 | ||
|
|
216b759df1 | ||
|
|
01450db71e | ||
|
|
ed987e1610 | ||
|
|
2773591e1f | ||
|
|
a995276d1e | ||
|
|
ffb6a8fa3f | ||
|
|
0966efb7f2 | ||
|
|
4a0a708092 | ||
|
|
6bf3b6f3e0 | ||
|
|
8f197fe266 | ||
|
|
e1a3f680f2 | ||
|
|
e89cca7e90 | ||
|
|
5bb2767d62 | ||
|
|
95f029e0e7 | ||
|
|
fb21e4d585 | ||
|
|
633805cec9 | ||
|
|
b8801d7d2a | ||
|
|
a84fac1b6a | ||
|
|
a9de4ceb30 | ||
|
|
827b55d60c | ||
|
|
0e1fe76b46 | ||
|
|
097c9e8e12 | ||
|
|
266356cb20 | ||
|
|
6dba39a804 | ||
|
|
3ddafa7aca | ||
|
|
9d8e232684 | ||
|
|
bf83c269c4 | ||
|
|
54e47c98cc | ||
|
|
118f2594ea | ||
|
|
ab4fcd6ac4 | ||
|
|
ca6f345429 | ||
|
|
79b8e1b4e4 | ||
|
|
cafa1ffa14 | ||
|
|
ea10df8a92 | ||
|
|
85abc42100 | ||
|
|
4747eb4386 | ||
|
|
ad9cc900b8 | ||
|
|
6cd93a7bb0 | ||
|
|
3338a02afb | ||
|
|
31cfe82224 | ||
|
|
70a191336b | ||
|
|
030477454c | ||
|
|
2a078d1572 | ||
|
|
3c1f96bc69 | ||
|
|
7d30bdc148 | ||
|
|
5d42961761 | ||
|
|
f20d5cd8d3 | ||
|
|
f5111c0f41 | ||
|
|
f5473236d0 | ||
|
|
d3cb31f1a7 | ||
|
|
d5f5cdd27a | ||
|
|
656dc8fefc | ||
|
|
a505cd9567 | ||
|
|
f2a860b01a | ||
|
|
1a5b93de9c | ||
|
|
0f165b33a6 | ||
|
|
4f53555f09 | ||
|
|
175be44823 | ||
|
|
20a6428290 | ||
|
|
4b4bea5f3b | ||
|
|
c82f860334 | ||
|
|
b2a56c047c | ||
|
|
bc6afc3933 | ||
|
|
280e4b65c3 | ||
|
|
c6f22f4d75 | ||
|
|
4bed8eb86f | ||
|
|
c482f18572 | ||
|
|
d7668acd9b | ||
|
|
05b978c568 | ||
|
|
62e5ab6990 | ||
|
|
12216f1463 | ||
|
|
cbfa13a8b4 | ||
|
|
03ec0cab1e | ||
|
|
d7940292d0 | ||
|
|
9139c5e9d6 | ||
|
|
65e62018e6 | ||
|
|
138a3673ce | ||
|
|
096f443b56 | ||
|
|
b37f30393d | ||
|
|
f095a05c42 | ||
|
|
3d15a73f1b | ||
|
|
bbd571e311 | ||
|
|
a7c554f033 | ||
|
|
25b4532ce3 | ||
|
|
a304f50a6b | ||
|
|
e75f476965 | ||
|
|
1c31460d27 | ||
|
|
19db468bf0 | ||
|
|
5fe05578c4 | ||
|
|
956f5a56cf | ||
|
|
a3f589d740 | ||
|
|
beef09bb6d | ||
|
|
ff0a246c28 | ||
|
|
f1459a1a52 | ||
|
|
f3501acb64 | ||
|
|
2238c98e95 | ||
|
|
9658d43f1f | ||
|
|
1748a0c3e5 | ||
|
|
4463d81758 | ||
|
|
74221a4ab5 | ||
|
|
0d998ed342 | ||
|
|
7d5a01756e | ||
|
|
d66313406b | ||
|
|
d96a267191 | ||
|
|
5467583bb3 | ||
|
|
9a8138d07b | ||
|
|
36c290ffea | ||
|
|
3413bf9f64 | ||
|
|
3c352a3545 | ||
|
|
56e4847b6b | ||
|
|
033b671d0b | ||
|
|
51f013851d | ||
|
|
dafa4ced27 | ||
|
|
05fe154749 | ||
|
|
5ddceb4660 | ||
|
|
341a1b195c | ||
|
|
29c3d1f9a6 | ||
|
|
734d4fb92b | ||
|
|
057a1cbab6 | ||
|
|
d44509b2cd | ||
|
|
58cf69795a | ||
|
|
6d39512576 | ||
|
|
ec4dde86f5 | ||
|
|
1c91fb9dd5 | ||
|
|
cbd650c5ef | ||
|
|
c5a769cb29 | ||
|
|
00a7277377 | ||
|
|
b8c32d41f5 | ||
|
|
49c9fc79c7 | ||
|
|
1284a7708e | ||
|
|
0dd8d15a9a | ||
|
|
90f59e954a | ||
|
|
03d7ec55a7 | ||
|
|
1929b69145 | ||
|
|
fbf194e4be | ||
|
|
a20927343a | ||
|
|
3b465dc09e | ||
|
|
fb8ca00ad1 | ||
|
|
dd7dddaa2b | ||
|
|
f41903c2a1 | ||
|
|
9984b5882d | ||
|
|
9ff20bee5a | ||
|
|
53caa11bc4 | ||
|
|
f6ac08567c | ||
|
|
040c6375c0 | ||
|
|
a03266aaad | ||
|
|
3479064348 | ||
|
|
b02d823b30 | ||
|
|
a204b6fb3f | ||
|
|
c2450843a5 | ||
|
|
00beb6170e | ||
|
|
9f1a300d2a | ||
|
|
05aefa1d5c | ||
|
|
059843f030 | ||
|
|
e202dc9851 | ||
|
|
18ae664ba7 | ||
|
|
76b563fa88 | ||
|
|
2553f4c161 | ||
|
|
f35c865348 | ||
|
|
b873ba3a75 | ||
|
|
d49e388ea3 | ||
|
|
b931699175 | ||
|
|
55fd58efd8 | ||
|
|
773847e139 |
@@ -16,7 +16,9 @@ Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
For instance, allows using Lodash's functions as decorators:
|
### `decorateWith(fn, ...args)`
|
||||||
|
|
||||||
|
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { decorateWith } from '@vates/decorate-with'
|
import { decorateWith } from '@vates/decorate-with'
|
||||||
@@ -29,6 +31,34 @@ class Foo {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `decorateMethodsWith(class, map)`
|
||||||
|
|
||||||
|
Decorates a number of methods directly, without using the decorator syntax:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { decorateMethodsWith } from '@vates/decorate-with'
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
bar() {
|
||||||
|
// body
|
||||||
|
}
|
||||||
|
|
||||||
|
baz() {
|
||||||
|
// body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decorateMethodsWith(Foo, {
|
||||||
|
// without arguments
|
||||||
|
bar: lodash.curry,
|
||||||
|
|
||||||
|
// with arguments
|
||||||
|
baz: [lodash.debounce, 150],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The decorated class is returned, so you can export it directly.
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Contributions are _very_ welcomed, either on the documentation or on
|
Contributions are _very_ welcomed, either on the documentation or on
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
For instance, allows using Lodash's functions as decorators:
|
### `decorateWith(fn, ...args)`
|
||||||
|
|
||||||
|
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { decorateWith } from '@vates/decorate-with'
|
import { decorateWith } from '@vates/decorate-with'
|
||||||
@@ -10,3 +12,31 @@ class Foo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `decorateMethodsWith(class, map)`
|
||||||
|
|
||||||
|
Decorates a number of methods directly, without using the decorator syntax:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { decorateMethodsWith } from '@vates/decorate-with'
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
bar() {
|
||||||
|
// body
|
||||||
|
}
|
||||||
|
|
||||||
|
baz() {
|
||||||
|
// body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decorateMethodsWith(Foo, {
|
||||||
|
// without arguments
|
||||||
|
bar: lodash.curry,
|
||||||
|
|
||||||
|
// with arguments
|
||||||
|
baz: [lodash.debounce, 150],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The decorated class is returned, so you can export it directly.
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
exports.decorateWith = (fn, ...args) => (target, name, descriptor) => ({
|
exports.decorateWith = function decorateWith(fn, ...args) {
|
||||||
|
return (target, name, descriptor) => ({
|
||||||
...descriptor,
|
...descriptor,
|
||||||
value: fn(descriptor.value, ...args),
|
value: fn(descriptor.value, ...args),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getOwnPropertyDescriptor, defineProperty } = Object
|
||||||
|
|
||||||
|
exports.decorateMethodsWith = function decorateMethodsWith(klass, map) {
|
||||||
|
const { prototype } = klass
|
||||||
|
for (const name of Object.keys(map)) {
|
||||||
|
const descriptor = getOwnPropertyDescriptor(prototype, name)
|
||||||
|
const { value } = descriptor
|
||||||
|
|
||||||
|
const decorator = map[name]
|
||||||
|
descriptor.value = typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1))
|
||||||
|
defineProperty(prototype, name, descriptor)
|
||||||
|
}
|
||||||
|
return klass
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"url": "https://vates.fr"
|
"url": "https://vates.fr"
|
||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.10"
|
"node": ">=8.10"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vates/multi-key-map": "^0.1.0",
|
"@vates/multi-key-map": "^0.1.0",
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/log": "^0.2.0",
|
"@xen-orchestra/log": "^0.3.0",
|
||||||
"ensure-array": "^1.0.0"
|
"ensure-array": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"main": "dist/",
|
"main": "dist/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,9 +30,8 @@
|
|||||||
"rimraf": "^3.0.0"
|
"rimraf": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vates/decorate-with": "^0.0.1",
|
"@vates/decorate-with": "^0.1.0",
|
||||||
"@xen-orchestra/log": "^0.2.0",
|
"@xen-orchestra/log": "^0.3.0",
|
||||||
"core-js": "^3.6.4",
|
|
||||||
"golike-defer": "^0.5.1",
|
"golike-defer": "^0.5.1",
|
||||||
"object-hash": "^2.0.1"
|
"object-hash": "^2.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// see https://github.com/babel/babel/issues/8450
|
|
||||||
import 'core-js/features/symbol/async-iterator'
|
|
||||||
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import hash from 'object-hash'
|
import hash from 'object-hash'
|
||||||
import { createLogger } from '@xen-orchestra/log'
|
import { createLogger } from '@xen-orchestra/log'
|
||||||
|
|||||||
@@ -56,14 +56,22 @@ module.exports = function (pkg, configs = {}) {
|
|||||||
}),
|
}),
|
||||||
presets: Object.keys(presets).map(preset => [preset, presets[preset]]),
|
presets: Object.keys(presets).map(preset => [preset, presets[preset]]),
|
||||||
targets: (() => {
|
targets: (() => {
|
||||||
|
const targets = {}
|
||||||
|
|
||||||
|
if (pkg.browserslist !== undefined) {
|
||||||
|
targets.browsers = pkg.browserslist
|
||||||
|
}
|
||||||
|
|
||||||
let node = (pkg.engines || {}).node
|
let node = (pkg.engines || {}).node
|
||||||
if (node !== undefined) {
|
if (node !== undefined) {
|
||||||
const trimChars = '^=>~'
|
const trimChars = '^=>~'
|
||||||
while (trimChars.includes(node[0])) {
|
while (trimChars.includes(node[0])) {
|
||||||
node = node.slice(1)
|
node = node.slice(1)
|
||||||
}
|
}
|
||||||
|
targets.node = node
|
||||||
}
|
}
|
||||||
return { browsers: pkg.browserslist, node }
|
|
||||||
|
return targets
|
||||||
})(),
|
})(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ const { resolve } = require('path')
|
|||||||
const adapter = new RemoteAdapter(require('@xen-orchestra/fs').getHandler({ url: 'file://' }))
|
const adapter = new RemoteAdapter(require('@xen-orchestra/fs').getHandler({ url: 'file://' }))
|
||||||
|
|
||||||
module.exports = async function main(args) {
|
module.exports = async function main(args) {
|
||||||
const { _, remove, merge } = getopts(args, {
|
const { _, fix, remove, merge } = getopts(args, {
|
||||||
alias: {
|
alias: {
|
||||||
|
fix: 'f',
|
||||||
remove: 'r',
|
remove: 'r',
|
||||||
merge: 'm',
|
merge: 'm',
|
||||||
},
|
},
|
||||||
boolean: ['merge', 'remove'],
|
boolean: ['fix', 'merge', 'remove'],
|
||||||
default: {
|
default: {
|
||||||
merge: false,
|
merge: false,
|
||||||
remove: false,
|
remove: false,
|
||||||
@@ -25,7 +26,7 @@ module.exports = async function main(args) {
|
|||||||
await asyncMap(_, async vmDir => {
|
await asyncMap(_, async vmDir => {
|
||||||
vmDir = resolve(vmDir)
|
vmDir = resolve(vmDir)
|
||||||
try {
|
try {
|
||||||
await adapter.cleanVm(vmDir, { remove, merge, onLog: log => console.warn(log) })
|
await adapter.cleanVm(vmDir, { fixMetadata: fix, remove, merge, onLog: (...args) => console.warn(...args) })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('adapter.cleanVm', vmDir, error)
|
console.error('adapter.cleanVm', vmDir, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ require('./_composeCommands')({
|
|||||||
get main() {
|
get main() {
|
||||||
return require('./commands/clean-vms')
|
return require('./commands/clean-vms')
|
||||||
},
|
},
|
||||||
usage: `[--merge] [--remove] xo-vm-backups/*
|
usage: `[--fix] [--merge] [--remove] xo-vm-backups/*
|
||||||
|
|
||||||
Detects and repair issues with VM backups.
|
Detects and repair issues with VM backups.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
-f, --fix Fix metadata issues (like size)
|
||||||
-m, --merge Merge (or continue merging) VHD files that are unused
|
-m, --merge Merge (or continue merging) VHD files that are unused
|
||||||
-r, --remove Remove unused, incomplete, orphan, or corrupted files
|
-r, --remove Remove unused, incomplete, orphan, or corrupted files
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/backups": "^0.11.0",
|
"@xen-orchestra/backups": "^0.13.0",
|
||||||
"@xen-orchestra/fs": "^0.17.0",
|
"@xen-orchestra/fs": "^0.18.0",
|
||||||
"filenamify": "^4.1.0",
|
"filenamify": "^4.1.0",
|
||||||
"getopts": "^2.2.5",
|
"getopts": "^2.2.5",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
|||||||
@@ -103,9 +103,21 @@ exports.VmBackup = class VmBackup {
|
|||||||
// calls fn for each function, warns of any errors, and throws only if there are no writers left
|
// calls fn for each function, warns of any errors, and throws only if there are no writers left
|
||||||
async _callWriters(fn, warnMessage, parallel = true) {
|
async _callWriters(fn, warnMessage, parallel = true) {
|
||||||
const writers = this._writers
|
const writers = this._writers
|
||||||
if (writers.size === 0) {
|
const n = writers.size
|
||||||
|
if (n === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (n === 1) {
|
||||||
|
const [writer] = writers
|
||||||
|
try {
|
||||||
|
await fn(writer)
|
||||||
|
} catch (error) {
|
||||||
|
writers.delete(writer)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
|
await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
|
||||||
try {
|
try {
|
||||||
await fn(writer)
|
await fn(writer)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
|
const sum = require('lodash/sum')
|
||||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||||
const { default: Vhd, mergeVhd } = require('vhd-lib')
|
const { default: Vhd, mergeVhd } = require('vhd-lib')
|
||||||
const { dirname, resolve } = require('path')
|
const { dirname, resolve } = require('path')
|
||||||
@@ -113,7 +114,7 @@ const listVhds = async (handler, vmDir) => {
|
|||||||
return { vhds, interruptedVhds }
|
return { vhds, interruptedVhds }
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop }) {
|
exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, onLog = noop }) {
|
||||||
const handler = this._handler
|
const handler = this._handler
|
||||||
|
|
||||||
const vhds = new Set()
|
const vhds = new Set()
|
||||||
@@ -219,11 +220,16 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
|
|||||||
await asyncMap(jsons, async json => {
|
await asyncMap(jsons, async json => {
|
||||||
const metadata = JSON.parse(await handler.readFile(json))
|
const metadata = JSON.parse(await handler.readFile(json))
|
||||||
const { mode } = metadata
|
const { mode } = metadata
|
||||||
|
let size
|
||||||
if (mode === 'full') {
|
if (mode === 'full') {
|
||||||
const linkedXva = resolve('/', vmDir, metadata.xva)
|
const linkedXva = resolve('/', vmDir, metadata.xva)
|
||||||
|
|
||||||
if (xvas.has(linkedXva)) {
|
if (xvas.has(linkedXva)) {
|
||||||
unusedXvas.delete(linkedXva)
|
unusedXvas.delete(linkedXva)
|
||||||
|
|
||||||
|
size = await handler.getSize(linkedXva).catch(error => {
|
||||||
|
onLog(`failed to get size of ${json}`, { error })
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
onLog(`the XVA linked to the metadata ${json} is missing`)
|
onLog(`the XVA linked to the metadata ${json} is missing`)
|
||||||
if (remove) {
|
if (remove) {
|
||||||
@@ -241,6 +247,10 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
|
|||||||
// possible (existing disks) even if one disk is missing
|
// possible (existing disks) even if one disk is missing
|
||||||
if (linkedVhds.every(_ => vhds.has(_))) {
|
if (linkedVhds.every(_ => vhds.has(_))) {
|
||||||
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
||||||
|
|
||||||
|
size = await asyncMap(linkedVhds, vhd => handler.getSize(vhd)).then(sum, error => {
|
||||||
|
onLog(`failed to get size of ${json}`, { error })
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
onLog(`Some VHDs linked to the metadata ${json} are missing`)
|
onLog(`Some VHDs linked to the metadata ${json} are missing`)
|
||||||
if (remove) {
|
if (remove) {
|
||||||
@@ -249,6 +259,22 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metadataSize = metadata.size
|
||||||
|
if (size !== undefined && metadataSize !== size) {
|
||||||
|
onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
|
||||||
|
|
||||||
|
// don't update if the the stored size is greater than found files,
|
||||||
|
// it can indicates a problem
|
||||||
|
if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
|
||||||
|
try {
|
||||||
|
metadata.size = size
|
||||||
|
await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
|
||||||
|
} catch (error) {
|
||||||
|
onLog(`failed to update size in backup metadata ${json}`, { error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: parallelize by vm/job/vdi
|
// TODO: parallelize by vm/job/vdi
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|||||||
blocked_operations: {
|
blocked_operations: {
|
||||||
...vmRecord.blocked_operations,
|
...vmRecord.blocked_operations,
|
||||||
start: 'Importing…',
|
start: 'Importing…',
|
||||||
|
start_on: 'Importing…',
|
||||||
},
|
},
|
||||||
ha_always_run: false,
|
ha_always_run: false,
|
||||||
is_a_template: false,
|
is_a_template: false,
|
||||||
@@ -305,9 +306,6 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Wait for VDI export tasks (if any) termination.
|
|
||||||
Promise.all(Object.values(streams).map(stream => stream.task)),
|
|
||||||
|
|
||||||
// Create VIFs.
|
// Create VIFs.
|
||||||
asyncMap(Object.values(deltaVm.vifs), vif => {
|
asyncMap(Object.values(deltaVm.vifs), vif => {
|
||||||
let network = vif.$network$uuid && xapi.getObjectByUuid(vif.$network$uuid, undefined)
|
let network = vif.$network$uuid && xapi.getObjectByUuid(vif.$network$uuid, undefined)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ const { execFile } = require('child_process')
|
|||||||
const parse = createParser({
|
const parse = createParser({
|
||||||
keyTransform: key => key.slice(5).toLowerCase(),
|
keyTransform: key => key.slice(5).toLowerCase(),
|
||||||
})
|
})
|
||||||
const makeFunction = command => async (fields, ...args) => {
|
const makeFunction =
|
||||||
|
command =>
|
||||||
|
async (fields, ...args) => {
|
||||||
const info = await fromCallback(execFile, command, [
|
const info = await fromCallback(execFile, command, [
|
||||||
'--noheading',
|
'--noheading',
|
||||||
'--nosuffix',
|
'--nosuffix',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||||
},
|
},
|
||||||
"version": "0.11.0",
|
"version": "0.13.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.6"
|
"node": ">=14.6"
|
||||||
},
|
},
|
||||||
@@ -20,25 +20,25 @@
|
|||||||
"@vates/disposable": "^0.1.1",
|
"@vates/disposable": "^0.1.1",
|
||||||
"@vates/parse-duration": "^0.1.1",
|
"@vates/parse-duration": "^0.1.1",
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/fs": "^0.17.0",
|
"@xen-orchestra/fs": "^0.18.0",
|
||||||
"@xen-orchestra/log": "^0.2.0",
|
"@xen-orchestra/log": "^0.3.0",
|
||||||
"@xen-orchestra/template": "^0.1.0",
|
"@xen-orchestra/template": "^0.1.0",
|
||||||
"compare-versions": "^3.6.0",
|
"compare-versions": "^3.6.0",
|
||||||
"d3-time-format": "^3.0.0",
|
"d3-time-format": "^3.0.0",
|
||||||
"end-of-stream": "^1.4.4",
|
"end-of-stream": "^1.4.4",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"golike-defer": "^0.5.1",
|
"golike-defer": "^0.5.1",
|
||||||
"limit-concurrency-decorator": "^0.5.0",
|
"limit-concurrency-decorator": "^0.5.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"node-zone": "^0.4.0",
|
"node-zone": "^0.4.0",
|
||||||
"parse-pairs": "^1.1.0",
|
"parse-pairs": "^1.1.0",
|
||||||
"pump": "^3.0.0",
|
|
||||||
"promise-toolbox": "^0.19.2",
|
"promise-toolbox": "^0.19.2",
|
||||||
"vhd-lib": "^1.0.0",
|
"pump": "^3.0.0",
|
||||||
|
"vhd-lib": "^1.2.0",
|
||||||
"yazl": "^2.5.1"
|
"yazl": "^2.5.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@xen-orchestra/xapi": "^0.6.2"
|
"@xen-orchestra/xapi": "^0.7.0"
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async transfer({ timestamp, deltaExport, sizeContainers }) {
|
async _transfer({ timestamp, deltaExport, sizeContainers }) {
|
||||||
const adapter = this._adapter
|
const adapter = this._adapter
|
||||||
const backup = this._backup
|
const backup = this._backup
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
|||||||
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
|
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
|
||||||
}
|
}
|
||||||
|
|
||||||
async transfer({ timestamp, deltaExport, sizeContainers }) {
|
async _transfer({ timestamp, deltaExport, sizeContainers }) {
|
||||||
const sr = this._sr
|
const sr = this._sr
|
||||||
const { job, scheduleId, vm } = this._backup
|
const { job, scheduleId, vm } = this._backup
|
||||||
|
|
||||||
@@ -106,9 +106,11 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
|||||||
targetVm.ha_restart_priority !== '' &&
|
targetVm.ha_restart_priority !== '' &&
|
||||||
Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
|
Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
|
||||||
targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
|
targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
|
||||||
|
asyncMap(['start', 'start_on'], op =>
|
||||||
targetVm.update_blocked_operations(
|
targetVm.update_blocked_operations(
|
||||||
'start',
|
op,
|
||||||
'Start operation for this vm is blocked, clone it if you want to use it.'
|
'Start operation for this vm is blocked, clone it if you want to use it.'
|
||||||
|
)
|
||||||
),
|
),
|
||||||
targetVm.update_other_config({
|
targetVm.update_other_config({
|
||||||
'xo:backup:sr': srUuid,
|
'xo:backup:sr': srUuid,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async run({ timestamp, sizeContainer, stream }) {
|
async _run({ timestamp, sizeContainer, stream }) {
|
||||||
const backup = this._backup
|
const backup = this._backup
|
||||||
const settings = this._settings
|
const settings = this._settings
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
|
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
|
||||||
const { asyncMapSettled } = require('@xen-orchestra/async-map')
|
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
||||||
const { formatDateTime } = require('@xen-orchestra/xapi')
|
const { formatDateTime } = require('@xen-orchestra/xapi')
|
||||||
|
|
||||||
const { formatFilenameDate } = require('../_filenameDate.js')
|
const { formatFilenameDate } = require('../_filenameDate.js')
|
||||||
@@ -29,7 +29,7 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async run({ timestamp, sizeContainer, stream }) {
|
async _run({ timestamp, sizeContainer, stream }) {
|
||||||
const sr = this._sr
|
const sr = this._sr
|
||||||
const settings = this._settings
|
const settings = this._settings
|
||||||
const { job, scheduleId, vm } = this._backup
|
const { job, scheduleId, vm } = this._backup
|
||||||
@@ -64,9 +64,11 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
|
|||||||
const targetVm = await xapi.getRecord('VM', targetVmRef)
|
const targetVm = await xapi.getRecord('VM', targetVmRef)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
asyncMap(['start', 'start_on'], op =>
|
||||||
targetVm.update_blocked_operations(
|
targetVm.update_blocked_operations(
|
||||||
'start',
|
op,
|
||||||
'Start operation for this vm is blocked, clone it if you want to use it.'
|
'Start operation for this vm is blocked, clone it if you want to use it.'
|
||||||
|
)
|
||||||
),
|
),
|
||||||
targetVm.update_other_config({
|
targetVm.update_other_config({
|
||||||
'xo:backup:sr': srUuid,
|
'xo:backup:sr': srUuid,
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ exports.AbstractDeltaWriter = class AbstractDeltaWriter extends AbstractWriter {
|
|||||||
throw new Error('Not implemented')
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer({ timestamp, deltaExport, sizeContainers }) {
|
async transfer({ timestamp, deltaExport, sizeContainers }) {
|
||||||
throw new Error('Not implemented')
|
try {
|
||||||
|
return await this._transfer({ timestamp, deltaExport, sizeContainers })
|
||||||
|
} finally {
|
||||||
|
// ensure all streams are properly closed
|
||||||
|
for (const stream of Object.values(deltaExport.streams)) {
|
||||||
|
stream.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
const { AbstractWriter } = require('./_AbstractWriter.js')
|
const { AbstractWriter } = require('./_AbstractWriter.js')
|
||||||
|
|
||||||
exports.AbstractFullWriter = class AbstractFullWriter extends AbstractWriter {
|
exports.AbstractFullWriter = class AbstractFullWriter extends AbstractWriter {
|
||||||
run({ timestamp, sizeContainer, stream }) {
|
async run({ timestamp, sizeContainer, stream }) {
|
||||||
throw new Error('Not implemented')
|
try {
|
||||||
|
return await this._run({ timestamp, sizeContainer, stream })
|
||||||
|
} finally {
|
||||||
|
// ensure stream is properly closed
|
||||||
|
stream.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|||||||
|
|
||||||
_cleanVm(options) {
|
_cleanVm(options) {
|
||||||
return this._adapter
|
return this._adapter
|
||||||
.cleanVm(getVmBackupDir(this._backup.vm.uuid), { ...options, onLog: warn, lock: false })
|
.cleanVm(getVmBackupDir(this._backup.vm.uuid), { ...options, fixMetadata: true, onLog: warn, lock: false })
|
||||||
.catch(warn)
|
.catch(warn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,11 @@ ${cliName} v${pkg.version}
|
|||||||
'xo:backup:sr': tgtSr.uuid,
|
'xo:backup:sr': tgtSr.uuid,
|
||||||
'xo:copy_of': srcSnapshotUuid,
|
'xo:copy_of': srcSnapshotUuid,
|
||||||
}),
|
}),
|
||||||
tgtVm.update_blocked_operations('start', 'Start operation for this vm is blocked, clone it if you want to use it.'),
|
Promise.all(
|
||||||
|
['start', 'start_on'].map(op =>
|
||||||
|
tgtVm.update_blocked_operations(op, 'Start operation for this vm is blocked, clone it if you want to use it.')
|
||||||
|
)
|
||||||
|
),
|
||||||
Promise.all(
|
Promise.all(
|
||||||
userDevices.map(userDevice => {
|
userDevices.map(userDevice => {
|
||||||
const srcDisk = srcDisks[userDevice]
|
const srcDisk = srcDisks[userDevice]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"golike-defer": "^0.5.1",
|
"golike-defer": "^0.5.1",
|
||||||
"xen-api": "^0.32.0"
|
"xen-api": "^0.34.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish"
|
"postversion": "npm publish"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": false,
|
"private": false,
|
||||||
"name": "@xen-orchestra/defined",
|
"name": "@xen-orchestra/defined",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "Utilities to help handling (possibly) undefined values",
|
"description": "Utilities to help handling (possibly) undefined values",
|
||||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/defined",
|
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/defined",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": false,
|
"private": false,
|
||||||
"name": "@xen-orchestra/emit-async",
|
"name": "@xen-orchestra/emit-async",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "Emit an event for async listeners to settle",
|
"description": "Emit an event for async listeners to settle",
|
||||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/emit-async",
|
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/emit-async",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": false,
|
"private": false,
|
||||||
"name": "@xen-orchestra/fs",
|
"name": "@xen-orchestra/fs",
|
||||||
"version": "0.17.0",
|
"version": "0.18.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"description": "The File System for Xen Orchestra backups.",
|
"description": "The File System for Xen Orchestra backups.",
|
||||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
|
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
"@vates/coalesce-calls": "^0.1.0",
|
"@vates/coalesce-calls": "^0.1.0",
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"aws-sdk": "^2.686.0",
|
"aws-sdk": "^2.686.0",
|
||||||
"decorator-synchronized": "^0.5.0",
|
"decorator-synchronized": "^0.6.0",
|
||||||
"execa": "^5.0.0",
|
"execa": "^5.0.0",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"get-stream": "^6.0.0",
|
"get-stream": "^6.0.0",
|
||||||
"limit-concurrency-decorator": "^0.5.0",
|
"limit-concurrency-decorator": "^0.5.0",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"async-iterator-to-stream": "^1.1.0",
|
"async-iterator-to-stream": "^1.1.0",
|
||||||
"babel-plugin-lodash": "^3.3.2",
|
"babel-plugin-lodash": "^3.3.2",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"rimraf": "^3.0.0"
|
"rimraf": "^3.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import asyncMapSettled from '@xen-orchestra/async-map/legacy'
|
import asyncMapSettled from '@xen-orchestra/async-map/legacy'
|
||||||
import getStream from 'get-stream'
|
import getStream from 'get-stream'
|
||||||
import path, { basename } from 'path'
|
import path, { basename } from 'path'
|
||||||
import synchronized from 'decorator-synchronized'
|
|
||||||
import { coalesceCalls } from '@vates/coalesce-calls'
|
import { coalesceCalls } from '@vates/coalesce-calls'
|
||||||
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
|
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
|
||||||
import { limitConcurrency } from 'limit-concurrency-decorator'
|
import { limitConcurrency } from 'limit-concurrency-decorator'
|
||||||
import { parse } from 'xo-remote-parser'
|
import { parse } from 'xo-remote-parser'
|
||||||
import { pipeline } from 'stream'
|
import { pipeline } from 'stream'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
|
import { synchronized } from 'decorator-synchronized'
|
||||||
|
|
||||||
import normalizePath from './_normalizePath'
|
import normalizePath from './_normalizePath'
|
||||||
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
|
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
|
||||||
|
|||||||
@@ -27,3 +27,12 @@ export const getHandler = (remote, ...rest) => {
|
|||||||
}
|
}
|
||||||
return new Handler(remote, ...rest)
|
return new Handler(remote, ...rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSyncedHandler = async (...opts) => {
|
||||||
|
const handler = getHandler(...opts)
|
||||||
|
await handler.sync()
|
||||||
|
return {
|
||||||
|
dispose: () => handler.forget(),
|
||||||
|
value: handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
6
@xen-orchestra/lite/.babelrc.js
Normal file
6
@xen-orchestra/lite/.babelrc.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = require('../../@xen-orchestra/babel-config')(require('./package.json'), {
|
||||||
|
'@babel/preset-env': {
|
||||||
|
exclude: ['@babel/plugin-proposal-dynamic-import', '@babel/plugin-transform-regenerator'],
|
||||||
|
modules: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
32
@xen-orchestra/lite/.eslintrc.js
Normal file
32
@xen-orchestra/lite/.eslintrc.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
|
||||||
|
sourceType: 'module', // Allows for the use of imports
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true, // Allows for the parsing of JSX
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: '17',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
|
||||||
|
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'eslint-comments/disable-enable-pair': 'off',
|
||||||
|
// Necessary to pass empty Effects/State to Reaclette
|
||||||
|
'@typescript-eslint/no-empty-interface': 'off',
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/issues/1071
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
|
||||||
|
'@typescript-eslint/no-use-before-define': ['error'],
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
24
@xen-orchestra/lite/.gitignore
vendored
Normal file
24
@xen-orchestra/lite/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
71
@xen-orchestra/lite/package.json
Normal file
71
@xen-orchestra/lite/package.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "xo-lite",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.13.1",
|
||||||
|
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.0",
|
||||||
|
"@babel/plugin-proposal-optional-chaining": "^7.13.0",
|
||||||
|
"@babel/preset-env": "^7.13.5",
|
||||||
|
"@babel/preset-react": "^7.12.13",
|
||||||
|
"@babel/preset-typescript": "^7.13.0",
|
||||||
|
"@emotion/react": "^11.4.1",
|
||||||
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
|
"@mui/icons-material": "^5.0.0",
|
||||||
|
"@mui/lab": "^5.0.0-alpha.48",
|
||||||
|
"@mui/material": "^5.0.1",
|
||||||
|
"@novnc/novnc": "^1.2.0",
|
||||||
|
"@types/immutable": "^3.8.7",
|
||||||
|
"@types/js-cookie": "^2.2.6",
|
||||||
|
"@types/lodash": "^4.14.175",
|
||||||
|
"@types/node": "^14.14.21",
|
||||||
|
"@types/react": "^17.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0",
|
||||||
|
"@types/react-helmet": "^6.1.5",
|
||||||
|
"@types/react-intl": "^3.0.0",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
"@types/react-syntax-highlighter": "^13.5.0",
|
||||||
|
"@types/styled-components": "^5.1.9",
|
||||||
|
"@types/webpack-env": "^1.16.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.16.1",
|
||||||
|
"@typescript-eslint/parser": "^4.16.1",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"copy-webpack-plugin": "^10.2.0",
|
||||||
|
"eslint": "^7.21.0",
|
||||||
|
"eslint-plugin-react": "^7.22.0",
|
||||||
|
"html-webpack-plugin": "^5.2.0",
|
||||||
|
"human-format": "^0.11.0",
|
||||||
|
"immutable": "^4.0.0-rc.12",
|
||||||
|
"iterable-backoff": "^0.1.0",
|
||||||
|
"json-rpc-protocol": "^0.13.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"node-polyfill-webpack-plugin": "^1.0.3",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"promise-toolbox": "^0.16.0",
|
||||||
|
"reaclette": "^0.10.0",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
|
"react-intl": "^5.10.16",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-syntax-highlighter": "^15.4.3",
|
||||||
|
"styled-components": "^5.2.1",
|
||||||
|
"typescript": "^4.3.1",
|
||||||
|
"webpack": "^5.24.2",
|
||||||
|
"webpack-cli": "^4.9.1",
|
||||||
|
"xen-api": "^0.34.3"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"styled-components": "^5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NODE_ENV=production webpack",
|
||||||
|
"start": "cross-env NODE_ENV=development webpack serve",
|
||||||
|
"start:open": "npm run start -- --open"
|
||||||
|
},
|
||||||
|
"browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead"
|
||||||
|
}
|
||||||
BIN
@xen-orchestra/lite/public/favicon.ico
Normal file
BIN
@xen-orchestra/lite/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
13
@xen-orchestra/lite/public/index.html
Normal file
13
@xen-orchestra/lite/public/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="Xen Orchestra Lite" />
|
||||||
|
<title>XO Lite</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
@xen-orchestra/lite/public/logo.png
Normal file
BIN
@xen-orchestra/lite/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
90
@xen-orchestra/lite/src/App/Infrastructure.tsx
Normal file
90
@xen-orchestra/lite/src/App/Infrastructure.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Switch, Route, RouteComponentProps } from 'react-router-dom'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
import { withRouter } from 'react-router'
|
||||||
|
|
||||||
|
import Pool from './Pool'
|
||||||
|
import TabConsole from './TabConsole'
|
||||||
|
import TreeView from './TreeView'
|
||||||
|
|
||||||
|
import { ObjectsByType } from '../libs/xapi'
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
const LeftPanel = styled.div`
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-width: 15em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
width: 20%;
|
||||||
|
`
|
||||||
|
// FIXME: temporary work-around while investigating flew-grow issue:
|
||||||
|
// `overflow: hidden` forces the console to shrink to the max available width
|
||||||
|
// even when the tree component takes more than 20% of the width due to
|
||||||
|
// `min-width`
|
||||||
|
const MainPanel = styled.div`
|
||||||
|
overflow: hidden;
|
||||||
|
width: 80%;
|
||||||
|
`
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
pool?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
selectedObject?: string
|
||||||
|
selectedVm?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// For compatibility with 'withRouter'
|
||||||
|
interface Props extends RouteComponentProps {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
initialize: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const selectedNodesToArray = (nodes: Array<string> | string | undefined) =>
|
||||||
|
nodes === undefined ? undefined : Array.isArray(nodes) ? nodes : [nodes]
|
||||||
|
|
||||||
|
const Infrastructure = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: props => ({
|
||||||
|
selectedVm: props.location.pathname.split('/')[3],
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
selectedObject: (state, props) =>
|
||||||
|
props.location.pathname.startsWith('/infrastructure/pool') ? state.pool : state.selectedVm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state: { pool, selectedObject } }) => (
|
||||||
|
<Container>
|
||||||
|
<LeftPanel>
|
||||||
|
<TreeView defaultSelectedNodes={selectedNodesToArray(selectedObject)} />
|
||||||
|
</LeftPanel>
|
||||||
|
<MainPanel>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path={`/infrastructure/pool/${pool}/dashboard`}>
|
||||||
|
<Pool id={pool} />
|
||||||
|
</Route>
|
||||||
|
<Route
|
||||||
|
path='/infrastructure/vms/:id/console'
|
||||||
|
render={({
|
||||||
|
match: {
|
||||||
|
params: { id },
|
||||||
|
},
|
||||||
|
}) => <TabConsole key={id} vmId={id} />}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</MainPanel>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withRouter(Infrastructure)
|
||||||
120
@xen-orchestra/lite/src/App/Pool/dashboard/ObjectStatus.tsx
Normal file
120
@xen-orchestra/lite/src/App/Pool/dashboard/ObjectStatus.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import Icon from '../../../components/Icon'
|
||||||
|
import IntlMessage from '../../../components/IntlMessage'
|
||||||
|
import ProgressCircle from '../../../components/ProgressCircle'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nActive?: number
|
||||||
|
nTotal?: number
|
||||||
|
type: 'host' | 'VM'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
nInactive?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CAPTION_STYLE = { textTransform: 'uppercase', mt: 2 }
|
||||||
|
const TYPOGRAPHY_SX = { mb: 2 }
|
||||||
|
|
||||||
|
const ObjectStatusContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: row;
|
||||||
|
align-content: space-between;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CircularProgressPanel = styled.div`
|
||||||
|
margin-left: 2em;
|
||||||
|
`
|
||||||
|
|
||||||
|
const GridPanel = styled.div`
|
||||||
|
margin-left: 2em;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
// TODO: Add a loading page when data is not loaded as it is in the model(figma).
|
||||||
|
// FIXME: replace the hard-coded colors with the theme colors.
|
||||||
|
const ObjectStatus = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
nInactive: (state, { nTotal = 0, nActive = 0 }) => nTotal - nActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state: { nInactive }, nActive = 0, nTotal = 0, type }) => {
|
||||||
|
if (nTotal === 0) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<IntlMessage id={type === 'VM' ? 'noVms' : 'noHosts'} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectStatusContainer>
|
||||||
|
<CircularProgressPanel>
|
||||||
|
<ProgressCircle max={nTotal} value={nActive} />
|
||||||
|
</CircularProgressPanel>
|
||||||
|
<GridPanel>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography sx={TYPOGRAPHY_SX} variant='h5' component='div'>
|
||||||
|
<IntlMessage id={type === 'VM' ? 'vms' : 'hosts'} />
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={10}>
|
||||||
|
<Icon icon='circle' htmlColor='#00BA34' />
|
||||||
|
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
<IntlMessage id='active' />
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2}>
|
||||||
|
<Typography variant='body2' component='div'>
|
||||||
|
{nActive}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={10}>
|
||||||
|
<Icon icon='circle' htmlColor='#E8E8E8' />
|
||||||
|
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
<IntlMessage id='inactive' />
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2}>
|
||||||
|
<Typography variant='body2' component='div'>
|
||||||
|
{nInactive}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={10}>
|
||||||
|
<Typography variant='caption' component='div' sx={DEFAULT_CAPTION_STYLE}>
|
||||||
|
<IntlMessage id='total' />
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2}>
|
||||||
|
<Typography variant='caption' component='div' sx={DEFAULT_CAPTION_STYLE}>
|
||||||
|
{nTotal}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</GridPanel>
|
||||||
|
</ObjectStatusContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ObjectStatus
|
||||||
79
@xen-orchestra/lite/src/App/Pool/dashboard/index.tsx
Normal file
79
@xen-orchestra/lite/src/App/Pool/dashboard/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import ObjectStatus from './ObjectStatus'
|
||||||
|
|
||||||
|
import IntlMessage from '../../../components/IntlMessage'
|
||||||
|
import { Host, ObjectsByType, Vm } from '../../../libs/xapi'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType?: ObjectsByType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hosts?: Map<string, Host>
|
||||||
|
nRunningHosts?: number
|
||||||
|
nRunningVms?: number
|
||||||
|
vms?: Map<string, Vm>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const DEFAULT_STYLE = { m: 2 }
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: row;
|
||||||
|
align-content: space-between;
|
||||||
|
gap: 1.25em;
|
||||||
|
background: '#E8E8E8';
|
||||||
|
`
|
||||||
|
|
||||||
|
const Panel = styled.div`
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
box-shadow: 0px 1px 1px 0px #00000014, 0px 2px 1px 0px #0000000f, 0px 1px 3px 0px #0000001a;
|
||||||
|
margin: 0.5em;
|
||||||
|
`
|
||||||
|
const getHostPowerState = (host: Host) => {
|
||||||
|
const { $metrics } = host
|
||||||
|
return $metrics ? ($metrics.live ? 'Running' : 'Halted') : 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dashboard = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
hosts: state => state.objectsByType?.get('host'),
|
||||||
|
vms: state =>
|
||||||
|
state.objectsByType
|
||||||
|
?.get('VM')
|
||||||
|
?.filter((vm: Vm) => !vm.is_control_domain && !vm.is_a_snapshot && !vm.is_a_template),
|
||||||
|
nRunningHosts: state => (state.hosts?.filter((host: Host) => getHostPowerState(host) === 'Running')).size,
|
||||||
|
nRunningVms: state => (state.vms?.filter((vm: Vm) => vm.power_state === 'Running')).size,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state: { hosts, nRunningHosts, nRunningVms, vms } }) => (
|
||||||
|
<Container>
|
||||||
|
<Panel>
|
||||||
|
<Typography variant='h4' component='div' sx={DEFAULT_STYLE}>
|
||||||
|
<IntlMessage id='status' />
|
||||||
|
</Typography>
|
||||||
|
<ObjectStatus nActive={nRunningHosts} nTotal={hosts?.size} type='host' />
|
||||||
|
<Divider variant='middle' sx={DEFAULT_STYLE} />
|
||||||
|
<ObjectStatus nActive={nRunningVms} nTotal={vms?.size} type='VM' />
|
||||||
|
</Panel>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Dashboard
|
||||||
46
@xen-orchestra/lite/src/App/Pool/index.tsx
Normal file
46
@xen-orchestra/lite/src/App/Pool/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import Dashboard from './dashboard'
|
||||||
|
import Icon from '../../components/Icon'
|
||||||
|
import PanelHeader from '../../components/PanelHeader'
|
||||||
|
import { ObjectsByType, Pool as PoolType } from '../../libs/xapi'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
pool?: PoolType
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add tabs when https://github.com/vatesfr/xen-orchestra/pull/6096 is merged.
|
||||||
|
const Pool = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
pool: (state, props) => state.objectsByType?.get('pool')?.get(props.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state: { pool } }) => (
|
||||||
|
<>
|
||||||
|
<PanelHeader>
|
||||||
|
<span>
|
||||||
|
<Icon icon='warehouse' color='primary' /> {pool?.name_label}
|
||||||
|
</span>
|
||||||
|
</PanelHeader>
|
||||||
|
<Dashboard />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Pool
|
||||||
65
@xen-orchestra/lite/src/App/PoolTab/PoolNetworks.tsx
Normal file
65
@xen-orchestra/lite/src/App/PoolTab/PoolNetworks.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Map } from 'immutable'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import IntlMessage from '../../components/IntlMessage'
|
||||||
|
import Table, { Column } from '../../components/Table'
|
||||||
|
import { ObjectsByType, Pif } from '../../libs/xapi'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
objectsFetched: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
poolId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
managementPifs?: Pif[]
|
||||||
|
pifs?: Map<string, Pif>
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLUMNS: Column<Pif>[] = [
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='device' />,
|
||||||
|
render: pif => pif.device,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='dns' />,
|
||||||
|
render: pif => pif.DNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='gateway' />,
|
||||||
|
render: pif => pif.gateway,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='ip' />,
|
||||||
|
render: pif => pif.IP,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const PoolNetworks = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
managementPifs: state =>
|
||||||
|
state.pifs
|
||||||
|
?.filter(pif => pif.management)
|
||||||
|
.map(pif => ({ ...pif, id: pif.$id }))
|
||||||
|
.valueSeq()
|
||||||
|
.toArray(),
|
||||||
|
pifs: state => state.objectsByType.get('PIF'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state }) => (
|
||||||
|
<Table collection={state.managementPifs} columns={COLUMNS} placeholder={<IntlMessage id='noManagementPifs' />} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PoolNetworks
|
||||||
89
@xen-orchestra/lite/src/App/PoolTab/PoolUpdates.tsx
Normal file
89
@xen-orchestra/lite/src/App/PoolTab/PoolUpdates.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import humanFormat from 'human-format'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import IntlMessage from '../../components/IntlMessage'
|
||||||
|
import Table, { Column } from '../../components/Table'
|
||||||
|
import XapiConnection, { ObjectsByType, PoolUpdate } from '../../libs/xapi'
|
||||||
|
|
||||||
|
const COLUMN: Column<PoolUpdate>[] = [
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='name' />,
|
||||||
|
render: update => update.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='description' />,
|
||||||
|
render: update => update.description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='version' />,
|
||||||
|
render: update => update.version,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='release' />,
|
||||||
|
render: update => update.release,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: <IntlMessage id='size' />,
|
||||||
|
render: update => humanFormat.bytes(update.size),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
objectsFetched: boolean
|
||||||
|
xapi: XapiConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hostRef: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
availableUpdates?: PoolUpdate[] | JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const PoolUpdates = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
availableUpdates: async function (state, { hostRef }) {
|
||||||
|
try {
|
||||||
|
const stringifiedPoolUpdates = (await state.xapi.call(
|
||||||
|
'host.call_plugin',
|
||||||
|
hostRef,
|
||||||
|
'updater.py',
|
||||||
|
'check_update',
|
||||||
|
{}
|
||||||
|
)) as string
|
||||||
|
return JSON.parse(stringifiedPoolUpdates)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return <IntlMessage id='errorOccurred' />
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state: { availableUpdates } }) =>
|
||||||
|
availableUpdates !== undefined ? (
|
||||||
|
Array.isArray(availableUpdates) ? (
|
||||||
|
<>
|
||||||
|
{availableUpdates.length !== 0 && (
|
||||||
|
<IntlMessage id='availableUpdates' values={{ nUpdates: availableUpdates.length }} />
|
||||||
|
)}
|
||||||
|
<Table collection={availableUpdates} columns={COLUMN} placeholder={<IntlMessage id='noUpdatesAvailable' />} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
availableUpdates
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<IntlMessage id='loading' />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PoolUpdates
|
||||||
53
@xen-orchestra/lite/src/App/PoolTab/index.tsx
Normal file
53
@xen-orchestra/lite/src/App/PoolTab/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Map } from 'immutable'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import PoolNetworks from './PoolNetworks'
|
||||||
|
import PoolUpdates from './PoolUpdates'
|
||||||
|
|
||||||
|
import IntlMessage from '../../components/IntlMessage'
|
||||||
|
import { Host, ObjectsByType, Pool } from '../../libs/xapi'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsFetched: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
hosts?: Map<string, Host>
|
||||||
|
pool?: Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
const PoolTab = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
hosts: state => (state.objectsFetched ? state.objectsByType?.get('host') : undefined),
|
||||||
|
pool: state => (state.objectsFetched ? state.objectsByType?.get('pool')?.first() : undefined),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state }) =>
|
||||||
|
state.pool !== undefined ? (
|
||||||
|
<>
|
||||||
|
<PoolNetworks poolId={state.pool.$id} />
|
||||||
|
{state.hosts?.valueSeq().map(host => (
|
||||||
|
<div key={host.$id}>
|
||||||
|
<p>{host.name_label}</p>
|
||||||
|
<PoolUpdates hostRef={host.$ref} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<IntlMessage id='loading' />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PoolTab
|
||||||
110
@xen-orchestra/lite/src/App/Signin/index.tsx
Normal file
110
@xen-orchestra/lite/src/App/Signin/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import Checkbox from '../../components/Checkbox'
|
||||||
|
import Input from '../../components/Input'
|
||||||
|
import IntlMessage from '../../components/IntlMessage'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
password: string
|
||||||
|
rememberMe: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
interface ParentEffects {
|
||||||
|
connectToXapi: (password: string, rememberMe: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
setRememberMe: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
setPassword: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
submit: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Form = styled.form`
|
||||||
|
width: 20em;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Fieldset = styled.fieldset`
|
||||||
|
border: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const RememberMe = styled(Fieldset)`
|
||||||
|
text-align: start;
|
||||||
|
vertical-align: baseline;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Error = styled.p`
|
||||||
|
color: #a33;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Signin = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: () => ({
|
||||||
|
password: '',
|
||||||
|
rememberMe: false,
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
setRememberMe: function ({ currentTarget: { checked: rememberMe } }) {
|
||||||
|
this.state.rememberMe = rememberMe
|
||||||
|
},
|
||||||
|
setPassword: function ({ currentTarget: { value: password } }) {
|
||||||
|
this.state.password = password
|
||||||
|
},
|
||||||
|
submit: function () {
|
||||||
|
this.effects.connectToXapi(this.state.password, this.state.rememberMe)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ effects, state }) => (
|
||||||
|
<Wrapper>
|
||||||
|
<Form onSubmit={e => e.preventDefault()}>
|
||||||
|
<img src='logo.png' />
|
||||||
|
<h1>Xen Orchestra Lite</h1>
|
||||||
|
<Fieldset>
|
||||||
|
<Input disabled label={<IntlMessage id='login' />} value='root' />
|
||||||
|
</Fieldset>
|
||||||
|
<Fieldset>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
label={<IntlMessage id='password' />}
|
||||||
|
onChange={effects.setPassword}
|
||||||
|
type='password'
|
||||||
|
value={state.password}
|
||||||
|
/>
|
||||||
|
</Fieldset>
|
||||||
|
<RememberMe>
|
||||||
|
<label>
|
||||||
|
<Checkbox onChange={effects.setRememberMe} checked={state.rememberMe} />
|
||||||
|
|
||||||
|
<IntlMessage id='rememberMe' />
|
||||||
|
</label>
|
||||||
|
</RememberMe>
|
||||||
|
<Error>{state.error}</Error>
|
||||||
|
<Button type='submit' onClick={effects.submit}>
|
||||||
|
<IntlMessage id='connect' />
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Signin
|
||||||
300
@xen-orchestra/lite/src/App/StyleGuide/index.tsx
Normal file
300
@xen-orchestra/lite/src/App/StyleGuide/index.tsx
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
// https://mui.com/components/material-icons/
|
||||||
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle'
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
|
import { materialDark as codeStyle } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
|
import { toNumber } from 'lodash'
|
||||||
|
import { SelectChangeEvent } from '@mui/material'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import ActionButton from '../../components/ActionButton'
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import Checkbox from '../../components/Checkbox'
|
||||||
|
import Icon from '../../components/Icon'
|
||||||
|
import Input from '../../components/Input'
|
||||||
|
import ProgressCircle from '../../components/ProgressCircle'
|
||||||
|
import Select from '../../components/Select'
|
||||||
|
import Tabs from '../../components/Tabs'
|
||||||
|
import { alert, confirm } from '../../components/Modal'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
progressBarValue: number
|
||||||
|
value: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
onChangeProgressBarValue: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
onChangeSelect: (e: SelectChangeEvent<unknown>) => void
|
||||||
|
sayHello: () => void
|
||||||
|
sendPromise: (data: Record<string, unknown>) => Promise<void>
|
||||||
|
showAlertModal: () => void
|
||||||
|
showConfirmModal: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const Page = styled.div`
|
||||||
|
margin: 30px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
column-gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Render = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
border: solid 1px gray;
|
||||||
|
border-radius: 3px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Code = styled(SyntaxHighlighter).attrs(() => ({
|
||||||
|
language: 'jsx',
|
||||||
|
style: codeStyle,
|
||||||
|
}))`
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 0 !important;
|
||||||
|
`
|
||||||
|
|
||||||
|
const App = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: () => ({
|
||||||
|
progressBarValue: 100,
|
||||||
|
value: '',
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
onChangeProgressBarValue: function (e) {
|
||||||
|
this.state.progressBarValue = toNumber(e.target.value)
|
||||||
|
},
|
||||||
|
onChangeSelect: function (e) {
|
||||||
|
this.state.value = e.target.value
|
||||||
|
},
|
||||||
|
sayHello: () => alert('hello'),
|
||||||
|
sendPromise: data =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve()
|
||||||
|
window.alert(data.foo)
|
||||||
|
}, 1000)
|
||||||
|
}),
|
||||||
|
showAlertModal: () => alert({ message: 'This is an alert modal', title: 'Alert modal', icon: 'info' }),
|
||||||
|
showConfirmModal: () =>
|
||||||
|
confirm({
|
||||||
|
message: 'This is a confirm modal test',
|
||||||
|
title: 'Confirm modal',
|
||||||
|
icon: 'download',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ effects, state }) => (
|
||||||
|
<Page>
|
||||||
|
<h2>ActionButton</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<ActionButton data-foo='forwarded data props' onClick={effects.sendPromise}>
|
||||||
|
Send promise
|
||||||
|
</ActionButton>
|
||||||
|
</Render>
|
||||||
|
<Code>
|
||||||
|
{`<ActionButton data-foo='forwarded data props' onClick={effects.sendPromise}>
|
||||||
|
Send promise
|
||||||
|
</ActionButton>`}
|
||||||
|
</Code>
|
||||||
|
</Container>
|
||||||
|
<h2>Button</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<Button color='primary' onClick={effects.sayHello} startIcon={<AccountCircleIcon />}>
|
||||||
|
Primary
|
||||||
|
</Button>
|
||||||
|
<Button color='secondary' endIcon={<DeleteIcon />} onClick={effects.sayHello}>
|
||||||
|
Secondary
|
||||||
|
</Button>
|
||||||
|
<Button color='success' onClick={effects.sayHello}>
|
||||||
|
Success
|
||||||
|
</Button>
|
||||||
|
<Button color='warning' onClick={effects.sayHello}>
|
||||||
|
Warning
|
||||||
|
</Button>
|
||||||
|
<Button color='error' onClick={effects.sayHello}>
|
||||||
|
Error
|
||||||
|
</Button>
|
||||||
|
<Button color='info' onClick={effects.sayHello}>
|
||||||
|
Info
|
||||||
|
</Button>
|
||||||
|
</Render>
|
||||||
|
<Code>{`<Button color='primary' onClick={doSomething} startIcon={<AccountCircleIcon />}>
|
||||||
|
Primary
|
||||||
|
</Button>
|
||||||
|
<Button color='secondary' endIcon={<DeleteIcon />} onClick={doSomething}>
|
||||||
|
Secondary
|
||||||
|
</Button>
|
||||||
|
<Button color='success' onClick={doSomething}>
|
||||||
|
Success
|
||||||
|
</Button>
|
||||||
|
<Button color='warning' onClick={doSomething}>
|
||||||
|
Warning
|
||||||
|
</Button>
|
||||||
|
<Button color='error' onClick={doSomething}>
|
||||||
|
Error
|
||||||
|
</Button>
|
||||||
|
<Button color='info' onClick={doSomething}>
|
||||||
|
Info
|
||||||
|
</Button>`}</Code>
|
||||||
|
</Container>
|
||||||
|
<h2>Icon</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<Icon icon='truck' htmlColor='#0085FF' />
|
||||||
|
<Icon icon='truck' color='primary' size='2x' />
|
||||||
|
</Render>
|
||||||
|
<Code>{`// https://fontawesome.com/icons
|
||||||
|
<Icon icon='truck' htmlColor='#0085FF'/>
|
||||||
|
<Icon icon='truck' color='primary' size='2x' />`}</Code>
|
||||||
|
</Container>
|
||||||
|
<h2>Input</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<Input label='Input' />
|
||||||
|
<Checkbox />
|
||||||
|
</Render>
|
||||||
|
<Code>{`<TextInput label='Input' />
|
||||||
|
<Checkbox />`}</Code>
|
||||||
|
</Container>
|
||||||
|
<h2>Modal</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={effects.showAlertModal}
|
||||||
|
sx={{
|
||||||
|
marginBottom: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Alert
|
||||||
|
</Button>
|
||||||
|
<Button color='primary' onClick={effects.showConfirmModal}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</Render>
|
||||||
|
<Code>{`<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={() =>
|
||||||
|
alert({
|
||||||
|
message: 'This is an alert modal',
|
||||||
|
title: 'Alert modal',
|
||||||
|
icon: 'info'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Alert
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await confirm({
|
||||||
|
message: 'This is a confirm modal',
|
||||||
|
title: 'Confirm modal',
|
||||||
|
icon: 'download',
|
||||||
|
})
|
||||||
|
// The modal has been confirmed
|
||||||
|
} catch (reason) { // "cancel"
|
||||||
|
// The modal has been closed
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>`}</Code>
|
||||||
|
</Container>
|
||||||
|
<h2>ProgressCircle</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap' }}>
|
||||||
|
<div>
|
||||||
|
<ProgressCircle max={200} value={state.progressBarValue} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ProgressCircle max={200} showLabel={false} size={150} value={state.progressBarValue} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
defaultValue={state.progressBarValue}
|
||||||
|
max='200'
|
||||||
|
min='0'
|
||||||
|
onChange={effects.onChangeProgressBarValue}
|
||||||
|
step='1'
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
margin: '10px auto',
|
||||||
|
}}
|
||||||
|
type='range'
|
||||||
|
/>
|
||||||
|
</Render>
|
||||||
|
<Code>
|
||||||
|
{`<ProgressCircle max={200} value={state.progressBarValue} />
|
||||||
|
<ProgressCircle max={200} showLabel={false} size={150} value={state.progressBarValue} />`}
|
||||||
|
</Code>
|
||||||
|
</Container>
|
||||||
|
<h2>Select</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<Select
|
||||||
|
onChange={effects.onChangeSelect}
|
||||||
|
options={[
|
||||||
|
{ name: 'Bar', value: 1 },
|
||||||
|
{ name: 'Foo', value: 2 },
|
||||||
|
]}
|
||||||
|
value={state.value}
|
||||||
|
valueRenderer='value'
|
||||||
|
/>
|
||||||
|
</Render>
|
||||||
|
<Code>
|
||||||
|
{`<Select
|
||||||
|
onChange={handleChange}
|
||||||
|
optionRenderer={item => item.name}
|
||||||
|
options={[
|
||||||
|
{ name: 'Bar', value: 1 },
|
||||||
|
{ name: 'Foo', value: 2 },
|
||||||
|
]}
|
||||||
|
value={state.value}
|
||||||
|
valueRenderer='value'
|
||||||
|
/>`}
|
||||||
|
</Code>
|
||||||
|
</Container>
|
||||||
|
<h2>Tabs</h2>
|
||||||
|
<Container>
|
||||||
|
<Render>
|
||||||
|
<Tabs
|
||||||
|
tabs={[
|
||||||
|
{ component: 'Hello BAR!', label: 'BAR', pathname: '/styleguide' },
|
||||||
|
{ label: 'FOO', pathname: '/styleguide/foo' },
|
||||||
|
]}
|
||||||
|
useUrl
|
||||||
|
/>
|
||||||
|
</Render>
|
||||||
|
<Code>
|
||||||
|
{`<Tabs
|
||||||
|
tabs={[
|
||||||
|
{ component: 'Hello BAR!', label: 'BAR', pathname: '/styleguide' },
|
||||||
|
{ label: 'FOO', pathname: '/styleguide/foo' },
|
||||||
|
]}
|
||||||
|
useUrl
|
||||||
|
/>`}
|
||||||
|
</Code>
|
||||||
|
</Container>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default App
|
||||||
102
@xen-orchestra/lite/src/App/TabConsole.tsx
Normal file
102
@xen-orchestra/lite/src/App/TabConsole.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import Console from '../components/Console'
|
||||||
|
import IntlMessage, { translate } from '../components/IntlMessage'
|
||||||
|
import { ObjectsByType, Vm } from '../libs/xapi'
|
||||||
|
import PanelHeader from '../components/PanelHeader'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
consoleScale: number
|
||||||
|
sendCtrlAltDel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
vmId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
scaleConsole: React.ChangeEventHandler<HTMLInputElement>
|
||||||
|
setCtrlAltDel: (sendCtrlAltDel: State['sendCtrlAltDel']) => void
|
||||||
|
showNotImplemented: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
vm?: Vm
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabConsole = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: () => ({
|
||||||
|
// Value in percent
|
||||||
|
consoleScale: 100,
|
||||||
|
sendCtrlAltDel: undefined,
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
scaleConsole: function (e) {
|
||||||
|
this.state.consoleScale = +e.currentTarget.value
|
||||||
|
|
||||||
|
// With "scaleViewport", the canvas occupies all available space of its
|
||||||
|
// container. But when the size of the container is changed, the canvas
|
||||||
|
// size isn't updated
|
||||||
|
// Issue https://github.com/novnc/noVNC/issues/1364
|
||||||
|
// PR https://github.com/novnc/noVNC/pull/1365
|
||||||
|
window.dispatchEvent(new UIEvent('resize'))
|
||||||
|
},
|
||||||
|
setCtrlAltDel: function (sendCtrlAltDel) {
|
||||||
|
this.state.sendCtrlAltDel = sendCtrlAltDel
|
||||||
|
},
|
||||||
|
showNotImplemented: function () {
|
||||||
|
alert('Not Implemented')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
vm: (state, { vmId }) => state.objectsByType.get('VM')?.get(vmId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ effects, state, vmId }) => (
|
||||||
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<PanelHeader
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
key: 'start',
|
||||||
|
icon: 'play',
|
||||||
|
color: 'primary',
|
||||||
|
title: translate({ id: 'vmStartLabel' }),
|
||||||
|
variant: 'contained',
|
||||||
|
onClick: effects.showNotImplemented,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{state.vm?.name_label ?? 'loading'}{' '}
|
||||||
|
</PanelHeader>
|
||||||
|
|
||||||
|
{/* Hide scaling and Ctrl+Alt+Del button temporarily */}
|
||||||
|
{/* <RangeInput max={100} min={1} onChange={effects.scaleConsole} step={1} value={state.consoleScale} />
|
||||||
|
{state.sendCtrlAltDel !== undefined && (
|
||||||
|
<Button onClick={state.sendCtrlAltDel}>
|
||||||
|
<IntlMessage id='ctrlAltDel' />
|
||||||
|
</Button>
|
||||||
|
)} */}
|
||||||
|
{state.vm?.power_state !== 'Running' ? (
|
||||||
|
<p>
|
||||||
|
<IntlMessage id='consoleNotAvailable' />
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ flex: 1, overflow: 'hidden' }}>
|
||||||
|
<div style={{ height: '100%', width: '100%' }}>
|
||||||
|
<Console vmId={vmId} scale={state.consoleScale} setCtrlAltDel={effects.setCtrlAltDel} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TabConsole
|
||||||
131
@xen-orchestra/lite/src/App/TreeView.tsx
Normal file
131
@xen-orchestra/lite/src/App/TreeView.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Collection, Map } from 'immutable'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import Icon from '../components/Icon'
|
||||||
|
import IntlMessage from '../components/IntlMessage'
|
||||||
|
import Tree, { ItemType } from '../components/Tree'
|
||||||
|
import { Host, ObjectsByType, Pool, Vm } from '../libs/xapi'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
defaultSelectedNodes?: Array<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
collection?: Array<ItemType>
|
||||||
|
hostsByPool?: Collection.Keyed<string, Collection<string, Host>>
|
||||||
|
pools?: Map<string, Pool>
|
||||||
|
vms?: Map<string, Vm>
|
||||||
|
vmsByContainerRef?: Collection.Keyed<string, Collection<string, Vm>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHostPowerState = (host: Host) => {
|
||||||
|
const { $metrics } = host
|
||||||
|
return $metrics ? ($metrics.live ? 'Running' : 'Halted') : 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIconColor = (obj: Host | Vm) => {
|
||||||
|
const powerState = obj.power_state ?? getHostPowerState(obj as Host)
|
||||||
|
return powerState === 'Running' ? '#198754' : powerState === 'Halted' ? '#dc3545' : '#6c757d'
|
||||||
|
}
|
||||||
|
|
||||||
|
const TreeView = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
collection: state => {
|
||||||
|
if (state.pools === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const collection: ItemType[] = []
|
||||||
|
state.pools.valueSeq().forEach((pool: Pool) => {
|
||||||
|
const hosts = state.hostsByPool
|
||||||
|
?.get(pool.$id)
|
||||||
|
?.valueSeq()
|
||||||
|
.sortBy(host => host.name_label)
|
||||||
|
.map((host: Host) => ({
|
||||||
|
children: state.vmsByContainerRef
|
||||||
|
?.get(host.$ref)
|
||||||
|
?.valueSeq()
|
||||||
|
.sortBy(vm => vm.name_label)
|
||||||
|
.map((vm: Vm) => ({
|
||||||
|
id: vm.$id,
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<Icon icon='desktop' htmlColor={getIconColor(vm)} /> {vm.name_label}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
to: `/infrastructure/vms/${vm.$id}/console`,
|
||||||
|
tooltip: <IntlMessage id={vm.power_state.toLowerCase()} />,
|
||||||
|
}))
|
||||||
|
.toArray(),
|
||||||
|
id: host.$id,
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<Icon icon='server' htmlColor={getIconColor(host)} /> {host.name_label}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
tooltip: <IntlMessage id={getHostPowerState(host).toLowerCase()} />,
|
||||||
|
}))
|
||||||
|
.toArray()
|
||||||
|
|
||||||
|
const haltedVms = state.vmsByContainerRef
|
||||||
|
?.get(pool.$ref)
|
||||||
|
?.valueSeq()
|
||||||
|
.sortBy((vm: Vm) => vm.name_label)
|
||||||
|
.map((vm: Vm) => ({
|
||||||
|
id: vm.$id,
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<Icon icon='desktop' htmlColor={getIconColor(vm)} /> {vm.name_label}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
to: `/infrastructure/vms/${vm.$id}/console`,
|
||||||
|
tooltip: <IntlMessage id='halted' />,
|
||||||
|
}))
|
||||||
|
.toArray()
|
||||||
|
|
||||||
|
collection.push({
|
||||||
|
children: (hosts ?? []).concat(haltedVms ?? []),
|
||||||
|
id: pool.$id,
|
||||||
|
label: (
|
||||||
|
<span>
|
||||||
|
<Icon icon='warehouse' color='primary' /> {pool.name_label}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
to: `/infrastructure/pool/${pool.$id}/dashboard`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return collection
|
||||||
|
},
|
||||||
|
hostsByPool: state => state.objectsByType?.get('host')?.groupBy((host: Host) => host.$pool.$id),
|
||||||
|
pools: state => state.objectsByType?.get('pool'),
|
||||||
|
vms: state =>
|
||||||
|
state.objectsByType
|
||||||
|
?.get('VM')
|
||||||
|
?.filter((vm: Vm) => !vm.is_control_domain && !vm.is_a_snapshot && !vm.is_a_template),
|
||||||
|
vmsByContainerRef: state =>
|
||||||
|
state.vms?.groupBy(({ power_state: powerState, resident_on: host, $pool }: Vm) =>
|
||||||
|
powerState === 'Running' || powerState === 'Paused' ? host : $pool.$ref
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ state, defaultSelectedNodes }) =>
|
||||||
|
state.collection === undefined ? null : (
|
||||||
|
<div style={{ padding: '10px' }}>
|
||||||
|
<Tree collection={state.collection} defaultSelectedNodes={defaultSelectedNodes} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TreeView
|
||||||
506
@xen-orchestra/lite/src/App/index.tsx
Normal file
506
@xen-orchestra/lite/src/App/index.tsx
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
// import Badge from '@mui/material/Badge'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
|
||||||
|
import Container from '@mui/material/Container'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItem from '@mui/material/ListItem'
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton'
|
||||||
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu'
|
||||||
|
import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar'
|
||||||
|
import MuiDrawer from '@mui/material/Drawer'
|
||||||
|
import React from 'react'
|
||||||
|
import styledComponent from 'styled-components'
|
||||||
|
import Toolbar from '@mui/material/Toolbar'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { HashRouter as Router, Switch, Redirect, Route } from 'react-router-dom'
|
||||||
|
import { IntlProvider } from 'react-intl'
|
||||||
|
import { Map } from 'immutable'
|
||||||
|
import { styled, createTheme, ThemeProvider } from '@mui/material/styles'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
// import Button from '../components/Button'
|
||||||
|
import Icon from '../components/Icon'
|
||||||
|
import Infrastructure from './Infrastructure'
|
||||||
|
import IntlMessage from '../components/IntlMessage'
|
||||||
|
import Link from '../components/Link'
|
||||||
|
import messagesEn from '../lang/en.json'
|
||||||
|
import Modal from '../components/Modal'
|
||||||
|
import PoolTab from './PoolTab'
|
||||||
|
import Signin from './Signin/index'
|
||||||
|
import StyleGuide from './StyleGuide/index'
|
||||||
|
import TabConsole from './TabConsole'
|
||||||
|
|
||||||
|
import XapiConnection, { ObjectsByType, Pool, Vm } from '../libs/xapi'
|
||||||
|
|
||||||
|
const drawerWidth = 240
|
||||||
|
const redirectPaths = ['/', '/infrastructure']
|
||||||
|
|
||||||
|
interface AppBarProps extends MuiAppBarProps {
|
||||||
|
open?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Provided by this template: https://github.com/mui-org/material-ui/tree/next/docs/src/pages/getting-started/templates/dashboard
|
||||||
|
|
||||||
|
const AppBar = styled(MuiAppBar, {
|
||||||
|
shouldForwardProp: prop => prop !== 'open',
|
||||||
|
})<AppBarProps>(({ theme, open }) => ({
|
||||||
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
|
transition: theme.transitions.create(['width', 'margin'], {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
...(open && {
|
||||||
|
marginLeft: drawerWidth,
|
||||||
|
width: `calc(100% - ${drawerWidth}px)`,
|
||||||
|
transition: theme.transitions.create(['width', 'margin'], {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Drawer = styled(MuiDrawer, { shouldForwardProp: prop => prop !== 'open' })(({ theme, open }) => ({
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
position: 'relative',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: drawerWidth,
|
||||||
|
transition: theme.transitions.create('width', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
...(!open && {
|
||||||
|
overflowX: 'hidden',
|
||||||
|
transition: theme.transitions.create('width', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
width: theme.spacing(7),
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
width: theme.spacing(9),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const MainListItems = (): JSX.Element => (
|
||||||
|
<div>
|
||||||
|
<ListItemButton component='a' href='#infrastructure'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Icon icon='project-diagram' />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={<IntlMessage id='infrastructure' />} />
|
||||||
|
</ListItemButton>
|
||||||
|
<ListItemButton component='a' href='#about'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Icon icon='info-circle' />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary='About' />
|
||||||
|
</ListItemButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SecondaryListItemsParentState {}
|
||||||
|
|
||||||
|
interface SecondaryListItemsState {}
|
||||||
|
|
||||||
|
interface SecondaryListItemsProps {}
|
||||||
|
|
||||||
|
interface SecondaryListItemsParentEffects {}
|
||||||
|
|
||||||
|
interface SecondaryListItemsEffects {
|
||||||
|
disconnect: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecondaryListItemsComputed {}
|
||||||
|
|
||||||
|
const ICON_STYLE = { fontSize: '1.5em' }
|
||||||
|
|
||||||
|
const SecondaryListItems = withState<
|
||||||
|
SecondaryListItemsState,
|
||||||
|
SecondaryListItemsProps,
|
||||||
|
SecondaryListItemsEffects,
|
||||||
|
SecondaryListItemsComputed,
|
||||||
|
SecondaryListItemsParentState,
|
||||||
|
SecondaryListItemsParentEffects
|
||||||
|
>({}, ({ effects }) => (
|
||||||
|
<div>
|
||||||
|
<ListItem button onClick={() => effects.disconnect()}>
|
||||||
|
<ListItemIcon style={ICON_STYLE}>
|
||||||
|
<Icon icon='sign-out-alt' />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={<IntlMessage id='disconnect' />} />
|
||||||
|
</ListItem>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Default bootstrap 4 colors
|
||||||
|
// https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss#L67-L74
|
||||||
|
const mdTheme = createTheme({
|
||||||
|
background: {
|
||||||
|
primary: {
|
||||||
|
dark: '#111111',
|
||||||
|
light: '#FFFFFF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
error: {
|
||||||
|
main: '#dc3545',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
main: '#17a2b8',
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
dark: '#168FFF',
|
||||||
|
light: '#0085FF',
|
||||||
|
main: '#007bff',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#6c757d',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: '#28a745',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
main: '#ffc107',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiTab: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
color: '#E8E8E8',
|
||||||
|
fontStyle: 'medium',
|
||||||
|
fontSize: '1.25em',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: 'inter',
|
||||||
|
h1: {
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '3em',
|
||||||
|
fontStyle: 'medium',
|
||||||
|
lineHeight: '3.75em',
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '2.25em',
|
||||||
|
fontStyle: 'medium',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '1.5em',
|
||||||
|
fontStyle: 'medium',
|
||||||
|
lineHeight: '2em',
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '1.25em',
|
||||||
|
fontStyle: 'medium',
|
||||||
|
lineHeight: '1.75em',
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '1em',
|
||||||
|
fontStyle: 'medium',
|
||||||
|
lineHeight: '1.50em',
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.8em',
|
||||||
|
fontStyle: 'medium',
|
||||||
|
lineHeight: '1.25em',
|
||||||
|
},
|
||||||
|
caption: {
|
||||||
|
// styleName: Caps / Caps 1 - 14 Semi Bold
|
||||||
|
fontSize: '0.9em',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: '1.25em',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
// styleName: Paragraph / P2 - 16
|
||||||
|
fontSize: '1em',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: '1.5em',
|
||||||
|
letterSpacing: '0em',
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const FullPage = styledComponent.div`
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
xapi: XapiConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
connected: boolean
|
||||||
|
drawerOpen: boolean
|
||||||
|
error: React.ReactNode
|
||||||
|
xapiHostname: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
connectToXapi: (password: string, rememberMe: boolean) => void
|
||||||
|
disconnect: () => void
|
||||||
|
toggleDrawer: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
objectsFetched: boolean
|
||||||
|
pool?: Pool
|
||||||
|
url: string
|
||||||
|
vms?: Map<string, Vm>
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: () => ({
|
||||||
|
connected: Cookies.get('sessionId') !== undefined,
|
||||||
|
drawerOpen: false,
|
||||||
|
error: '',
|
||||||
|
objectsByType: undefined,
|
||||||
|
xapi: undefined,
|
||||||
|
xapiHostname: process.env.XAPI_HOST || window.location.host,
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
initialize: async function () {
|
||||||
|
const xapi = (this.state.xapi = new XapiConnection())
|
||||||
|
|
||||||
|
xapi.on('connected', () => {
|
||||||
|
this.state.connected = true
|
||||||
|
})
|
||||||
|
|
||||||
|
xapi.on('disconnected', () => {
|
||||||
|
this.state.connected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
xapi.on('objects', (objectsByType: ObjectsByType) => {
|
||||||
|
this.state.objectsByType = objectsByType
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await xapi.reattachSession(this.state.url)
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.code !== 'SESSION_INVALID') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Session ID is invalid. Asking for credentials.')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleDrawer: function () {
|
||||||
|
this.state.drawerOpen = !this.state.drawerOpen
|
||||||
|
},
|
||||||
|
connectToXapi: async function (password, rememberMe = false) {
|
||||||
|
try {
|
||||||
|
await this.state.xapi.connect({
|
||||||
|
url: this.state.url,
|
||||||
|
user: 'root',
|
||||||
|
password,
|
||||||
|
rememberMe,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.code !== 'SESSION_AUTHENTICATION_FAILED') {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.error = <IntlMessage id='badCredentials' />
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disconnect: async function () {
|
||||||
|
await this.state.xapi.disconnect()
|
||||||
|
this.state.connected = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
objectsFetched: state => state.objectsByType !== undefined,
|
||||||
|
pool: state => (state.objectsFetched ? state.objectsByType?.get('pool')?.keySeq().first() : undefined),
|
||||||
|
vms: state =>
|
||||||
|
state.objectsFetched
|
||||||
|
? state.objectsByType
|
||||||
|
?.get('VM')
|
||||||
|
?.filter((vm: Vm) => !vm.is_control_domain && !vm.is_a_snapshot && !vm.is_a_template)
|
||||||
|
: undefined,
|
||||||
|
url: state => `${window.location.protocol}//${state.xapiHostname}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ effects, state }) => (
|
||||||
|
<IntlProvider messages={messagesEn} locale='en'>
|
||||||
|
{/* Provided by this template: https://github.com/mui-org/material-ui/tree/next/docs/src/pages/getting-started/templates/dashboard */}
|
||||||
|
<ThemeProvider theme={mdTheme}>
|
||||||
|
<Modal />
|
||||||
|
{!state.connected ? (
|
||||||
|
<Signin />
|
||||||
|
) : !state.objectsFetched ? (
|
||||||
|
<IntlMessage id='loading' />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Router>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path={redirectPaths}>
|
||||||
|
<Redirect to={`/infrastructure/pool/${state.pool.$id}/dashboard`} />
|
||||||
|
</Route>
|
||||||
|
<Route exact path='/vm-list'>
|
||||||
|
{state.vms !== undefined && (
|
||||||
|
<>
|
||||||
|
<p>There are {state.vms.size} VMs!</p>
|
||||||
|
<ul>
|
||||||
|
{state.vms.valueSeq().map((vm: Vm) => (
|
||||||
|
<li key={vm.$id}>
|
||||||
|
<Link to={vm.$id}>
|
||||||
|
{vm.name_label} - {vm.name_description} ({vm.power_state})
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
<Route exact path='/styleguide'>
|
||||||
|
<StyleGuide />
|
||||||
|
</Route>
|
||||||
|
<Route exact path='/styleguide/foo'>
|
||||||
|
<StyleGuide />
|
||||||
|
</Route>
|
||||||
|
<Route exact path='/pool'>
|
||||||
|
<PoolTab />
|
||||||
|
</Route>
|
||||||
|
<Route path='/'>
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<CssBaseline />
|
||||||
|
<AppBar position='absolute' open={state.drawerOpen}>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
pr: '24px', // keep right padding when drawer closed
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
edge='start'
|
||||||
|
color='inherit'
|
||||||
|
aria-label='open drawer'
|
||||||
|
onClick={effects.toggleDrawer}
|
||||||
|
sx={{
|
||||||
|
marginRight: '36px',
|
||||||
|
...(state.drawerOpen && { display: 'none' }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography component='h1' variant='h6' color='inherit' noWrap sx={{ flexGrow: 1 }}>
|
||||||
|
<Switch>
|
||||||
|
<Route path='/infrastructure'>
|
||||||
|
<IntlMessage id='infrastructure' />
|
||||||
|
</Route>
|
||||||
|
<Route path='/about'>
|
||||||
|
<IntlMessage id='about' />
|
||||||
|
</Route>
|
||||||
|
<Route>
|
||||||
|
<IntlMessage id='notFound' />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Typography>
|
||||||
|
{/* <IconButton color='inherit'>
|
||||||
|
<Badge badgeContent={4} color='secondary'>
|
||||||
|
<NotificationsIcon />
|
||||||
|
</Badge>
|
||||||
|
</IconButton> */}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<Drawer variant='permanent' open={state.drawerOpen}>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
px: [1],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton onClick={effects.toggleDrawer}>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Toolbar>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
<MainListItems />
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
<SecondaryListItems />
|
||||||
|
</List>
|
||||||
|
</Drawer>
|
||||||
|
<Box
|
||||||
|
component='main'
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme =>
|
||||||
|
theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900],
|
||||||
|
flexGrow: 1,
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch>
|
||||||
|
<Route path='/infrastructure'>
|
||||||
|
<FullPage>
|
||||||
|
<Toolbar />
|
||||||
|
<Infrastructure />
|
||||||
|
</FullPage>
|
||||||
|
</Route>
|
||||||
|
<Route path='/about'>
|
||||||
|
<Toolbar />
|
||||||
|
<Container maxWidth='lg' sx={{ mt: 4, mb: 4 }}>
|
||||||
|
<p>
|
||||||
|
Check out{' '}
|
||||||
|
<Link to='https://xen-orchestra.com/blog/xen-orchestra-lite/'>Xen Orchestra Lite</Link>{' '}
|
||||||
|
dev blog.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<IntlMessage id='versionValue' values={{ version: process.env.NPM_VERSION }} />
|
||||||
|
</p>
|
||||||
|
</Container>
|
||||||
|
</Route>
|
||||||
|
<Route>
|
||||||
|
<Toolbar />
|
||||||
|
<IntlMessage id='pageNotFound' />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ThemeProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default App
|
||||||
57
@xen-orchestra/lite/src/components/ActionButton.tsx
Normal file
57
@xen-orchestra/lite/src/components/ActionButton.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import LoadingButton, { LoadingButtonProps } from '@mui/lab/LoadingButton'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Omit the `onClick` props to rewrite its own one.
|
||||||
|
interface Props extends Omit<LoadingButtonProps, 'onClick'> {
|
||||||
|
onClick: (data: Record<string, unknown>) => Promise<void>
|
||||||
|
// to pass props with the following pattern: "data-something"
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
_onClick: React.MouseEventHandler<HTMLButtonElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const ActionButton = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: () => ({ isLoading: false }),
|
||||||
|
effects: {
|
||||||
|
_onClick: function () {
|
||||||
|
this.state.isLoading = true
|
||||||
|
const data: Record<string, unknown> = {}
|
||||||
|
Object.keys(this.props).forEach(key => {
|
||||||
|
if (key.startsWith('data-')) {
|
||||||
|
data[key.slice(5)] = this.props[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return this.props.onClick(data).finally(() => (this.state.isLoading = false))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ children, color = 'secondary', effects, onClick, resetState, state, variant = 'contained', ...props }) => (
|
||||||
|
<LoadingButton
|
||||||
|
color={color}
|
||||||
|
disabled={state.isLoading}
|
||||||
|
fullWidth
|
||||||
|
loading={state.isLoading}
|
||||||
|
onClick={effects._onClick}
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LoadingButton>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ActionButton
|
||||||
26
@xen-orchestra/lite/src/components/Button.tsx
Normal file
26
@xen-orchestra/lite/src/components/Button.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Button as MuiButton, ButtonProps } from '@mui/material'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props extends ButtonProps {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const Button = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{},
|
||||||
|
({ children, color = 'secondary', effects, resetState, state, variant = 'contained', ...props }) => (
|
||||||
|
<MuiButton color={color} fullWidth variant={variant} {...props}>
|
||||||
|
{children}
|
||||||
|
</MuiButton>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Button
|
||||||
22
@xen-orchestra/lite/src/components/Checkbox.tsx
Normal file
22
@xen-orchestra/lite/src/components/Checkbox.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { CheckboxProps, Checkbox as MuiCheckbox } from '@mui/material'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props extends CheckboxProps {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const Checkbox = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{},
|
||||||
|
({ effects, resetState, state, ...props }) => <MuiCheckbox {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Checkbox
|
||||||
193
@xen-orchestra/lite/src/components/Console.tsx
Normal file
193
@xen-orchestra/lite/src/components/Console.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import RFB from '@novnc/novnc/lib/rfb'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { fibonacci } from 'iterable-backoff'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import IntlMessage from './IntlMessage'
|
||||||
|
import { confirm } from './Modal'
|
||||||
|
|
||||||
|
import XapiConnection, { ObjectsByType, Vm } from '../libs/xapi'
|
||||||
|
|
||||||
|
interface ParentState {
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
xapi: XapiConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
// Type error with HTMLDivElement.
|
||||||
|
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
|
||||||
|
container: React.RefObject<any>
|
||||||
|
// See https://github.com/vatesfr/xen-orchestra/pull/5722#discussion_r619296074
|
||||||
|
rfb: any
|
||||||
|
rfbConnected: boolean
|
||||||
|
timeout?: NodeJS.Timeout
|
||||||
|
tryToReconnect: boolean
|
||||||
|
url?: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
scale: number
|
||||||
|
setCtrlAltDel: (sendCtrlAltDel: Effects['sendCtrlAltDel']) => void
|
||||||
|
vmId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
_connect: () => Promise<void>
|
||||||
|
_handleConnect: () => void
|
||||||
|
_handleDisconnect: () => Promise<void>
|
||||||
|
sendCtrlAltDel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
interface PropsStyledConsole {
|
||||||
|
scale: number
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Protocols {
|
||||||
|
http = 'http:',
|
||||||
|
https = 'https:',
|
||||||
|
ws = 'ws:',
|
||||||
|
wss = 'wss:',
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledConsole = styled.div<PropsStyledConsole>`
|
||||||
|
height: ${props => props.scale}%;
|
||||||
|
margin: auto;
|
||||||
|
visibility: ${props => (props.visible ? 'visible' : 'hidden')};
|
||||||
|
width: ${props => props.scale}%;
|
||||||
|
`
|
||||||
|
|
||||||
|
// https://github.com/novnc/noVNC/blob/master/docs/API.md
|
||||||
|
const Console = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: () => ({
|
||||||
|
container: React.createRef(),
|
||||||
|
rfb: undefined,
|
||||||
|
rfbConnected: false,
|
||||||
|
timeout: undefined,
|
||||||
|
tryToReconnect: true,
|
||||||
|
url: undefined,
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
initialize: function () {
|
||||||
|
this.effects._connect()
|
||||||
|
},
|
||||||
|
_handleConnect: function () {
|
||||||
|
this.state.rfbConnected = true
|
||||||
|
},
|
||||||
|
_handleDisconnect: async function () {
|
||||||
|
this.state.rfbConnected = false
|
||||||
|
const {
|
||||||
|
state: { objectsByType, url },
|
||||||
|
effects: { _connect },
|
||||||
|
} = this
|
||||||
|
const { protocol } = window.location
|
||||||
|
if (protocol === Protocols.https) {
|
||||||
|
try {
|
||||||
|
await fetch(`${protocol}//${url?.host}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
try {
|
||||||
|
await confirm({
|
||||||
|
icon: 'exclamation-triangle',
|
||||||
|
message: (
|
||||||
|
<a href={`${protocol}//${url?.host}`} rel='noopener noreferrer' target='_blank'>
|
||||||
|
<IntlMessage
|
||||||
|
id='unreachableHost'
|
||||||
|
values={{
|
||||||
|
name: objectsByType.get('host')?.find(host => host.address === url?.host)?.name_label,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
title: <IntlMessage id='connectionError' />,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
this.state.tryToReconnect = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.tryToReconnect) {
|
||||||
|
_connect()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_connect: async function () {
|
||||||
|
const { vmId } = this.props
|
||||||
|
const { objectsByType, rfb, xapi } = this.state
|
||||||
|
let lastError: unknown
|
||||||
|
|
||||||
|
// 8 tries mean 54s
|
||||||
|
for (const delay of fibonacci().toMs().take(8)) {
|
||||||
|
try {
|
||||||
|
const consoles = (objectsByType.get('VM')?.get(vmId) as Vm)?.$consoles.filter(
|
||||||
|
vmConsole => vmConsole.protocol === 'rfb'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rfb !== undefined) {
|
||||||
|
rfb.removeEventListener('connect', this.effects._handleConnect)
|
||||||
|
rfb.removeEventListener('disconnect', this.effects._handleDisconnect)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consoles === undefined || consoles.length === 0) {
|
||||||
|
throw new Error('Could not find VM console')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xapi.sessionId === undefined) {
|
||||||
|
throw new Error('Not connected to XAPI')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.url = new URL(consoles[0].location)
|
||||||
|
this.state.url.protocol = window.location.protocol === Protocols.https ? Protocols.wss : Protocols.ws
|
||||||
|
this.state.url.searchParams.set('session_id', xapi.sessionId)
|
||||||
|
|
||||||
|
this.state.rfb = new RFB(this.state.container.current, this.state.url, {
|
||||||
|
wsProtocols: ['binary'],
|
||||||
|
})
|
||||||
|
this.state.rfb.addEventListener('connect', this.effects._handleConnect)
|
||||||
|
this.state.rfb.addEventListener('disconnect', this.effects._handleDisconnect)
|
||||||
|
this.state.rfb.scaleViewport = true
|
||||||
|
this.props.setCtrlAltDel(this.effects.sendCtrlAltDel)
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
await new Promise(resolve => (this.state.timeout = setTimeout(resolve, delay)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError
|
||||||
|
},
|
||||||
|
finalize: function () {
|
||||||
|
const { rfb, timeout } = this.state
|
||||||
|
rfb.removeEventListener('connect', this.effects._handleConnect)
|
||||||
|
rfb.removeEventListener('disconnect', this.effects._handleDisconnect)
|
||||||
|
if (timeout !== undefined) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendCtrlAltDel: async function () {
|
||||||
|
await confirm({
|
||||||
|
message: <IntlMessage id='confirmCtrlAltDel' />,
|
||||||
|
title: <IntlMessage id='ctrlAltDel' />,
|
||||||
|
})
|
||||||
|
this.state.rfb.sendCtrlAltDel()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ scale, state }) => (
|
||||||
|
<>
|
||||||
|
{state.rfb !== undefined && !state.rfbConnected && (
|
||||||
|
<p>
|
||||||
|
<IntlMessage id={state.tryToReconnect ? 'reconnectionAttempt' : 'hostUnreachable'} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<StyledConsole ref={state.container} scale={scale} visible={state.rfbConnected} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Console
|
||||||
30
@xen-orchestra/lite/src/components/Icon.tsx
Normal file
30
@xen-orchestra/lite/src/components/Icon.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { IconName as _IconName, library, SizeProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
library.add(fas)
|
||||||
|
|
||||||
|
const Icon = ({
|
||||||
|
color,
|
||||||
|
htmlColor,
|
||||||
|
icon,
|
||||||
|
size,
|
||||||
|
}: {
|
||||||
|
color?: 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning'
|
||||||
|
htmlColor?: string
|
||||||
|
icon: _IconName
|
||||||
|
size?: SizeProp
|
||||||
|
}): JSX.Element => {
|
||||||
|
const { palette } = useTheme()
|
||||||
|
return (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={icon}
|
||||||
|
size={size}
|
||||||
|
color={htmlColor ?? (color !== undefined ? palette[color][palette.mode] : undefined)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Icon
|
||||||
|
export type IconName = _IconName
|
||||||
26
@xen-orchestra/lite/src/components/Input.tsx
Normal file
26
@xen-orchestra/lite/src/components/Input.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { TextField, TextFieldProps } from '@mui/material'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
// An interface can only extend an object type or intersection
|
||||||
|
// of object types with statically known members.
|
||||||
|
type Props = _Props & TextFieldProps
|
||||||
|
|
||||||
|
interface _Props {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const Input = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{},
|
||||||
|
({ effects, resetState, state, ...props }) => <TextField fullWidth {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Input
|
||||||
21
@xen-orchestra/lite/src/components/IntlMessage.tsx
Normal file
21
@xen-orchestra/lite/src/components/IntlMessage.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React, { ElementType, ReactElement, ReactNode } from 'react'
|
||||||
|
import { FormattedMessage, MessageDescriptor, useIntl } from 'react-intl'
|
||||||
|
import intlMessage from '../lang/en.json'
|
||||||
|
|
||||||
|
// Extends FormattedMessage not working: "FormattedMessage refers to a value, but is being used as a type here"
|
||||||
|
// https://stackoverflow.com/questions/62059408/reactjs-and-typescript-refers-to-a-value-but-is-being-used-as-a-type-here-ts
|
||||||
|
// InstanceType<typeof FormattedMessage> not working: "Type [...] does not satisfy the constraint abstract new (...args: any) => any."
|
||||||
|
// See https://formatjs.io/docs/react-intl/components/#formattedmessage
|
||||||
|
interface Props extends MessageDescriptor {
|
||||||
|
children?: (chunks: ReactElement) => ReactElement
|
||||||
|
id?: keyof typeof intlMessage
|
||||||
|
tagName?: ElementType
|
||||||
|
values?: Record<string, ReactNode>
|
||||||
|
}
|
||||||
|
const IntlMessage = (props: Props): JSX.Element => <FormattedMessage {...props} />
|
||||||
|
|
||||||
|
export function translate(message: MessageDescriptor){
|
||||||
|
return useIntl().formatMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(IntlMessage)
|
||||||
38
@xen-orchestra/lite/src/components/Link.tsx
Normal file
38
@xen-orchestra/lite/src/components/Link.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import MaterialLink from '@mui/material/Link'
|
||||||
|
import React from 'react'
|
||||||
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode
|
||||||
|
decorated?: boolean
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const UNDECORATED_LINK = { textDecoration: 'none', color: 'inherit' }
|
||||||
|
|
||||||
|
const Link = withState<State, Props, Effects, Computed, ParentState, ParentEffects>({}, ({ to, decorated = true, children }) =>
|
||||||
|
to === undefined ? (
|
||||||
|
<>{children}</>
|
||||||
|
) : to.startsWith('http') ? (
|
||||||
|
<MaterialLink style={decorated ? undefined : UNDECORATED_LINK} target='_blank' rel='noopener noreferrer' href={to}>
|
||||||
|
{children}
|
||||||
|
</MaterialLink>
|
||||||
|
) : (
|
||||||
|
<RouterLink style={decorated ? undefined : UNDECORATED_LINK} component={MaterialLink} to={to}>
|
||||||
|
{children}
|
||||||
|
</RouterLink>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Link
|
||||||
152
@xen-orchestra/lite/src/components/Modal.tsx
Normal file
152
@xen-orchestra/lite/src/components/Modal.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { ButtonProps, Dialog, DialogContent, DialogContentText, DialogActions, DialogTitle } from '@mui/material'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import Button from './Button'
|
||||||
|
import Icon, { IconName } from './Icon'
|
||||||
|
import IntlMessage from './IntlMessage'
|
||||||
|
|
||||||
|
type ModalButton = {
|
||||||
|
color?: ButtonProps['color']
|
||||||
|
label: string | React.ReactNode
|
||||||
|
reason?: unknown
|
||||||
|
value?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneralParamsModal {
|
||||||
|
icon: IconName
|
||||||
|
message: string | React.ReactNode
|
||||||
|
title: string | React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalParams extends GeneralParamsModal {
|
||||||
|
buttonList: ModalButton[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance: EffectContext<State, Props, Effects, Computed, ParentState, ParentEffects> | undefined
|
||||||
|
const modal = ({ buttonList, icon, message, title }: ModalParams) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (instance === undefined) {
|
||||||
|
throw new Error('No modal instance')
|
||||||
|
}
|
||||||
|
instance.state.buttonList = buttonList
|
||||||
|
instance.state.icon = icon
|
||||||
|
instance.state.message = message
|
||||||
|
instance.state.onReject = reject
|
||||||
|
instance.state.onSuccess = resolve
|
||||||
|
instance.state.showModal = true
|
||||||
|
instance.state.title = title
|
||||||
|
})
|
||||||
|
|
||||||
|
export const alert = (params: GeneralParamsModal): Promise<unknown> => {
|
||||||
|
const buttonList: ModalButton[] = [
|
||||||
|
{
|
||||||
|
label: <IntlMessage id='ok' />,
|
||||||
|
color: 'primary',
|
||||||
|
value: 'success',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return modal({ ...params, buttonList })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirm = (params: GeneralParamsModal): Promise<unknown> => {
|
||||||
|
const buttonList: ModalButton[] = [
|
||||||
|
{
|
||||||
|
label: <IntlMessage id='confirm' />,
|
||||||
|
value: 'confirm',
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <IntlMessage id='cancel' />,
|
||||||
|
color: 'secondary',
|
||||||
|
reason: 'cancel',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return modal({ ...params, buttonList })
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
buttonList?: ModalButton[]
|
||||||
|
icon?: IconName
|
||||||
|
message?: string | React.ReactNode
|
||||||
|
onReject?: (reason: unknown) => void
|
||||||
|
onSuccess?: (value: unknown) => void
|
||||||
|
showModal: boolean
|
||||||
|
title?: string | React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
closeModal: () => void
|
||||||
|
reject: (reason: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const Modal = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: () => ({
|
||||||
|
buttonList: undefined,
|
||||||
|
icon: undefined,
|
||||||
|
message: undefined,
|
||||||
|
onReject: undefined,
|
||||||
|
onSuccess: undefined,
|
||||||
|
showModal: false,
|
||||||
|
title: undefined,
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
initialize: function () {
|
||||||
|
if (instance !== undefined) {
|
||||||
|
throw new Error('Modal is a singelton')
|
||||||
|
}
|
||||||
|
instance = this
|
||||||
|
},
|
||||||
|
closeModal: function () {
|
||||||
|
this.state.showModal = false
|
||||||
|
},
|
||||||
|
reject: function (reason) {
|
||||||
|
this.state.onReject?.(reason)
|
||||||
|
this.effects.closeModal()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ effects, state }) => {
|
||||||
|
const { closeModal, reject } = effects
|
||||||
|
const { buttonList, icon, message, onReject, onSuccess, showModal, title } = state
|
||||||
|
|
||||||
|
return showModal ? (
|
||||||
|
<Dialog open={showModal} onClose={reject}>
|
||||||
|
<DialogTitle>
|
||||||
|
{icon !== undefined && <Icon icon={icon} />} {title}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{message}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{buttonList?.map(({ label, reason, value, ...props }, index) => {
|
||||||
|
const onClick = () => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
onSuccess?.(value)
|
||||||
|
} else {
|
||||||
|
onReject?.(reason)
|
||||||
|
}
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button key={index} onClick={onClick} {...props}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Modal
|
||||||
63
@xen-orchestra/lite/src/components/PanelHeader.tsx
Normal file
63
@xen-orchestra/lite/src/components/PanelHeader.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import Icon, { IconName } from './Icon'
|
||||||
|
|
||||||
|
import Button, { ButtonProps } from '@mui/material/Button'
|
||||||
|
import ButtonGroup, { ButtonGroupClassKey } from '@mui/material/ButtonGroup'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import Typography, { TypographyClassKey } from '@mui/material/Typography'
|
||||||
|
import { Theme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Action extends ButtonProps {
|
||||||
|
icon: IconName
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const DEFAULT_TITLE_STYLE = { marginLeft: '0.5em', flex: 1, fontSize: '250%' }
|
||||||
|
const DEFAULT_BUTTONGROUP_STYLE = { margin: '0.5em', flex: 0 }
|
||||||
|
const DEFAULT_STACK_STYLE = {
|
||||||
|
backgroundColor: (theme: Theme) => {
|
||||||
|
const { background, palette } = theme
|
||||||
|
return palette.mode === 'light' ? background.primary.light : background.primary.dark
|
||||||
|
},
|
||||||
|
paddingTop: '1em',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
// Accepts an array of Actions. An action accepts all the props of a Button + an icon
|
||||||
|
actions?: Array<Action>
|
||||||
|
// the props passed to the title, accepts all the keys of Typography
|
||||||
|
titleProps?: TypographyClassKey
|
||||||
|
// the props passed to the button group, accepts all the keys of a ButtonGroup
|
||||||
|
buttonGroupProps?: ButtonGroupClassKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const PanelHeader = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{},
|
||||||
|
({ actions = [], titleProps = {}, buttonGroupProps = {}, children = null }) => (
|
||||||
|
<Stack direction='row' justifyContent='space-between' alignItems='center' sx={DEFAULT_STACK_STYLE}>
|
||||||
|
<Typography variant='h2' sx={DEFAULT_TITLE_STYLE} {...titleProps}>
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
<ButtonGroup sx={DEFAULT_BUTTONGROUP_STYLE} {...buttonGroupProps}>
|
||||||
|
{(actions as Array<Action>)?.map(({ icon, ...actionProps }) => (
|
||||||
|
<Button {...actionProps} key={actionProps.key}>
|
||||||
|
<Icon icon={icon} />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</ButtonGroup>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PanelHeader
|
||||||
87
@xen-orchestra/lite/src/components/ProgressCircle.tsx
Normal file
87
@xen-orchestra/lite/src/components/ProgressCircle.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import React from 'react'
|
||||||
|
import CircularProgress, { CircularProgressProps } from '@mui/material/CircularProgress'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import { Typography } from '@mui/material'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
const BackgroundBox = styled(Box)({
|
||||||
|
position: 'absolute',
|
||||||
|
})
|
||||||
|
|
||||||
|
const BackgroundCircle = styled(CircularProgress)({
|
||||||
|
color: '#e3dede',
|
||||||
|
})
|
||||||
|
|
||||||
|
const Container = styled(Box)({
|
||||||
|
display: 'inline-flex',
|
||||||
|
position: 'relative',
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledLabel = styled(Typography)(({ color, theme: { palette } }) => ({
|
||||||
|
color: (palette[(color as string) ?? 'primary'] ?? palette.primary).main,
|
||||||
|
textAlign: 'center',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const LabelBox = styled(Box)({
|
||||||
|
alignItems: 'center',
|
||||||
|
bottom: 0,
|
||||||
|
display: 'flex',
|
||||||
|
height: '80%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
left: 0,
|
||||||
|
margin: 'auto',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: '80%',
|
||||||
|
})
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
color?: CircularProgressProps['color']
|
||||||
|
label?: string
|
||||||
|
max?: number
|
||||||
|
showLabel?: boolean
|
||||||
|
size?: number
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
label: string
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgressCircle = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
label: ({ progress }, { label }) => label ?? `${progress}%`,
|
||||||
|
progress: (_, { max = 100, value }) => Math.round((value / max) * 100),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ color = 'success', showLabel = true, size = 100, state: { label, progress } }) => (
|
||||||
|
<Container>
|
||||||
|
<BackgroundBox>
|
||||||
|
<BackgroundCircle variant='determinate' value={100} size={size} />
|
||||||
|
</BackgroundBox>
|
||||||
|
<CircularProgress aria-label={label} color={color} size={size} value={progress} variant='determinate' />
|
||||||
|
{showLabel && (
|
||||||
|
<LabelBox>
|
||||||
|
<StyledLabel variant='h5' color={color}>
|
||||||
|
{label}
|
||||||
|
</StyledLabel>
|
||||||
|
</LabelBox>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ProgressCircle
|
||||||
7
@xen-orchestra/lite/src/components/RangeInput.tsx
Normal file
7
@xen-orchestra/lite/src/components/RangeInput.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type Props = Omit<React.ComponentPropsWithoutRef<'input'>, 'type'>
|
||||||
|
|
||||||
|
const RangeInput = React.memo((props: Props) => <input {...props} type='range' />)
|
||||||
|
|
||||||
|
export default RangeInput
|
||||||
97
@xen-orchestra/lite/src/components/Select.tsx
Normal file
97
@xen-orchestra/lite/src/components/Select.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import FormControl from '@mui/material/FormControl'
|
||||||
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
import React from 'react'
|
||||||
|
import SelectMaterialUi, { SelectProps } from '@mui/material/Select'
|
||||||
|
import { iteratee } from 'lodash'
|
||||||
|
import { SelectChangeEvent } from '@mui/material'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import IntlMessage from './IntlMessage'
|
||||||
|
|
||||||
|
type AdditionalProps = Record<string, any>
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props extends SelectProps {
|
||||||
|
additionalProps?: AdditionalProps
|
||||||
|
onChange: (e: SelectChangeEvent<unknown>) => void
|
||||||
|
optionRenderer?: string | { (item: any): number | string }
|
||||||
|
options: any[] | undefined
|
||||||
|
value: any
|
||||||
|
valueRenderer?: string | { (item: any): number | string }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
renderOption: (item: any, additionalProps?: AdditionalProps) => React.ReactNode
|
||||||
|
renderValue: (item: any, additionalProps?: AdditionalProps) => number | string
|
||||||
|
options?: JSX.Element[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Select = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
computed: {
|
||||||
|
// @ts-ignore
|
||||||
|
renderOption: (_, { optionRenderer }) => iteratee(optionRenderer),
|
||||||
|
// @ts-ignore
|
||||||
|
renderValue: (_, { valueRenderer }) => iteratee(valueRenderer),
|
||||||
|
options: (state, { additionalProps, options, optionRenderer, valueRenderer }) =>
|
||||||
|
options?.map(item => {
|
||||||
|
const label =
|
||||||
|
optionRenderer === undefined
|
||||||
|
? item.name ?? item.label ?? item.name_label
|
||||||
|
: state.renderOption(item, additionalProps)
|
||||||
|
const value =
|
||||||
|
valueRenderer === undefined ? item.value ?? item.id ?? item.$id : state.renderValue(item, additionalProps)
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
console.error('Computed value is undefined')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({
|
||||||
|
additionalProps,
|
||||||
|
displayEmpty = true,
|
||||||
|
effects,
|
||||||
|
multiple,
|
||||||
|
options,
|
||||||
|
required,
|
||||||
|
resetState,
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<FormControl>
|
||||||
|
<SelectMaterialUi
|
||||||
|
multiple={multiple}
|
||||||
|
required={required}
|
||||||
|
displayEmpty={displayEmpty}
|
||||||
|
value={value ?? (multiple ? [] : '')}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{!multiple && (
|
||||||
|
<MenuItem value=''>
|
||||||
|
<em>
|
||||||
|
<IntlMessage id='none' />
|
||||||
|
</em>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{state.options}
|
||||||
|
</SelectMaterialUi>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Select
|
||||||
73
@xen-orchestra/lite/src/components/Table.tsx
Normal file
73
@xen-orchestra/lite/src/components/Table.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
|
||||||
|
import IntlMessage from './IntlMessage'
|
||||||
|
|
||||||
|
export type Column<Type> = {
|
||||||
|
header: React.ReactNode
|
||||||
|
id?: string
|
||||||
|
render: { (item: Type): React.ReactNode }
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
id?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
collection: Item[] | undefined
|
||||||
|
columns: Column<any>[]
|
||||||
|
placeholder?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
const StyledTable = styled.table`
|
||||||
|
border: 1px solid #333;
|
||||||
|
td {
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const Table = withState<State, Props, Effects, Computed, ParentState, ParentEffects>({}, ({ collection, columns, placeholder }) =>
|
||||||
|
collection !== undefined ? (
|
||||||
|
collection.length !== 0 ? (
|
||||||
|
<StyledTable>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((col, index) => (
|
||||||
|
<td key={col.id ?? index}>{col.header}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{collection.map((item, index) => (
|
||||||
|
<tr key={item.id ?? index}>
|
||||||
|
{columns.map((col, index) => (
|
||||||
|
<td key={col.id ?? index}>{col.render(item)}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</StyledTable>
|
||||||
|
) : (
|
||||||
|
placeholder ?? <IntlMessage id='noData' />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<IntlMessage id='loading' />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Table
|
||||||
114
@xen-orchestra/lite/src/components/Tabs.tsx
Normal file
114
@xen-orchestra/lite/src/components/Tabs.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import React from 'react'
|
||||||
|
import Tab from '@mui/material/Tab'
|
||||||
|
import TabContext from '@mui/lab/TabContext'
|
||||||
|
import TabList from '@mui/lab/TabList'
|
||||||
|
import TabPanel from '@mui/lab/TabPanel'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { RouteComponentProps } from 'react-router-dom'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
import { withRouter } from 'react-router'
|
||||||
|
|
||||||
|
import IntlMessage from '../components/IntlMessage'
|
||||||
|
|
||||||
|
const BOX_STYLE = { borderBottom: 1, borderColor: 'divider', marginTop: '0.5em' }
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
component?: React.ReactNode
|
||||||
|
disabled?: boolean
|
||||||
|
label: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UrlTab extends Tab {
|
||||||
|
pathname: string
|
||||||
|
value?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NoUrlTab extends Tab {
|
||||||
|
value: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// For compatibility with 'withRouter'
|
||||||
|
interface Props extends RouteComponentProps {
|
||||||
|
indicatorColor?: 'primary' | 'secondary'
|
||||||
|
textColor?: 'inherit' | 'primary' | 'secondary'
|
||||||
|
// tabs = [
|
||||||
|
// {
|
||||||
|
// component: <span>BAR</span>,
|
||||||
|
// pathname: '/path',
|
||||||
|
// label: (
|
||||||
|
// <span>
|
||||||
|
// <Icon icon='cloud' /> {labelA}
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
tabs: Array<NoUrlTab | UrlTab>
|
||||||
|
useUrl?: boolean
|
||||||
|
value?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
onChange: (event: React.SyntheticEvent, value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {}
|
||||||
|
|
||||||
|
// TODO: improve view as done in the model(figma).
|
||||||
|
const pageUnderConstruction = (
|
||||||
|
<div style={{ color: '#0085FF', textAlign: 'center' }}>
|
||||||
|
<Typography variant='h2'>
|
||||||
|
<IntlMessage id='xoLiteUnderConstruction' />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h3'>
|
||||||
|
<IntlMessage id='newFeaturesUnderConstruction' />
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const Tabs = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: ({ location: { pathname }, tabs, useUrl = false, value }) => ({
|
||||||
|
value: (useUrl && pathname) || (value ?? tabs[0].value ?? tabs[0].pathname),
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
onChange: function (_, value) {
|
||||||
|
if (this.props.useUrl) {
|
||||||
|
const { history, tabs } = this.props
|
||||||
|
history.push(tabs.find(tab => (tab.value ?? tab.pathname) === value).pathname)
|
||||||
|
}
|
||||||
|
this.state.value = value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ effects, state: { value }, indicatorColor, textColor, tabs }) => (
|
||||||
|
<TabContext value={value}>
|
||||||
|
<Box sx={BOX_STYLE}>
|
||||||
|
<TabList indicatorColor={indicatorColor} onChange={effects.onChange} textColor={textColor}>
|
||||||
|
{tabs.map((tab: UrlTab | NoUrlTab) => {
|
||||||
|
const value = tab.value ?? tab.pathname
|
||||||
|
return <Tab disabled={tab.disabled} key={value} label={tab.label} value={value} />
|
||||||
|
})}
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
{tabs.map((tab: UrlTab | NoUrlTab) => {
|
||||||
|
const value = tab.value ?? tab.pathname
|
||||||
|
return (
|
||||||
|
<TabPanel key={value} value={value}>
|
||||||
|
{tab.component === undefined ? pageUnderConstruction : tab.component}
|
||||||
|
</TabPanel>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TabContext>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withRouter(Tabs)
|
||||||
196
@xen-orchestra/lite/src/components/Tree.tsx
Normal file
196
@xen-orchestra/lite/src/components/Tree.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import classNames from 'classnames'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
|
import TreeView from '@mui/lab/TreeView'
|
||||||
|
import TreeItem, { useTreeItem, TreeItemContentProps } from '@mui/lab/TreeItem'
|
||||||
|
import { withState } from 'reaclette'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
import Icon from '../components/Icon'
|
||||||
|
|
||||||
|
interface ParentState {}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
expandedNodes?: Array<string>
|
||||||
|
selectedNodes?: Array<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemType {
|
||||||
|
children?: Array<ItemType>
|
||||||
|
id: string
|
||||||
|
label: React.ReactElement
|
||||||
|
to?: string
|
||||||
|
tooltip?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// collection = [
|
||||||
|
// {
|
||||||
|
// id: 'idA',
|
||||||
|
// label: (
|
||||||
|
// <span>
|
||||||
|
// <Icon icon='warehouse' /> {labelA}
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// to: '/routeA',
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// id: 'ida',
|
||||||
|
// label: label: (
|
||||||
|
// <span>
|
||||||
|
// <Icon icon='server' /> {labela}
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'idB',
|
||||||
|
// label: (
|
||||||
|
// <span>
|
||||||
|
// <Icon icon='warehouse' /> {labelB}
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// to: '/routeB',
|
||||||
|
// tooltip: <IntlMessage id='tooltipB' />
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
collection: Array<ItemType>
|
||||||
|
defaultSelectedNodes?: Array<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomContentProps extends TreeItemContentProps {
|
||||||
|
defaultSelectedNode?: string
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentEffects {}
|
||||||
|
|
||||||
|
interface Effects {
|
||||||
|
setExpandedNodeIds: (event: React.SyntheticEvent, nodeIds: Array<string>) => void
|
||||||
|
setSelectedNodeIds: (event: React.SyntheticEvent, nodeIds: Array<string>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Computed {
|
||||||
|
defaultSelectedNode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspired by https://mui.com/components/tree-view/#contentcomponent-prop.
|
||||||
|
const CustomContent = React.forwardRef(function CustomContent(props: CustomContentProps, ref) {
|
||||||
|
const { classes, className, defaultSelectedNode, expansionIcon, label, nodeId, to } = props
|
||||||
|
const { focused, handleExpansion, handleSelection, selected } = useTreeItem(nodeId)
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// There can only be one node selected at once for now.
|
||||||
|
// Auto-revealing more than one node in the tree would require a different implementation.
|
||||||
|
if (defaultSelectedNode === nodeId) {
|
||||||
|
ref?.current?.scrollIntoView()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected) {
|
||||||
|
to !== undefined && history.push(to)
|
||||||
|
}
|
||||||
|
}, [selected])
|
||||||
|
|
||||||
|
const handleExpansionClick = (event: React.SyntheticEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
handleExpansion(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(className, { [classes.focused]: focused, [classes.selected]: selected })}
|
||||||
|
onClick={handleSelection}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<span className={classes.iconContainer} onClick={handleExpansionClick}>
|
||||||
|
{expansionIcon}
|
||||||
|
</span>
|
||||||
|
<span className={classes.label}>{label}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderItem = ({ children, id, label, to, tooltip }: ItemType, defaultSelectedNode?: string) => {
|
||||||
|
return (
|
||||||
|
<TreeItem
|
||||||
|
ContentComponent={CustomContent}
|
||||||
|
// FIXME: ContentProps should only be React.HTMLAttributes<HTMLElement> or undefined, it doesn't support other type.
|
||||||
|
// when https://github.com/mui-org/material-ui/issues/28668 is fixed, remove 'as CustomContentProps'.
|
||||||
|
ContentProps={{ defaultSelectedNode, to } as CustomContentProps}
|
||||||
|
label={tooltip ? <Tooltip title={tooltip}>{label}</Tooltip> : label}
|
||||||
|
key={id}
|
||||||
|
nodeId={id}
|
||||||
|
>
|
||||||
|
{Array.isArray(children) ? children.map(item => renderItem(item, defaultSelectedNode)) : null}
|
||||||
|
</TreeItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tree = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
{
|
||||||
|
initialState: ({ collection, defaultSelectedNodes }) => {
|
||||||
|
if (defaultSelectedNodes === undefined) {
|
||||||
|
return {
|
||||||
|
expandedNodes: [collection[0].id],
|
||||||
|
selectedNodes: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandedNodes should contain all nodes up to the defaultSelectedNodes.
|
||||||
|
const expandedNodes = new Set<string>()
|
||||||
|
const pathToNode = new Set<string>()
|
||||||
|
const addExpandedNode = (collection: Array<ItemType> | undefined) => {
|
||||||
|
if (collection === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of collection) {
|
||||||
|
if (defaultSelectedNodes.includes(node.id)) {
|
||||||
|
for (const nodeId of pathToNode) {
|
||||||
|
expandedNodes.add(nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pathToNode.add(node.id)
|
||||||
|
addExpandedNode(node.children)
|
||||||
|
pathToNode.delete(node.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addExpandedNode(collection)
|
||||||
|
|
||||||
|
return { expandedNodes: Array.from(expandedNodes), selectedNodes: defaultSelectedNodes }
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
setExpandedNodeIds: function (_, nodeIds) {
|
||||||
|
this.state.expandedNodes = nodeIds
|
||||||
|
},
|
||||||
|
setSelectedNodeIds: function (_, nodeIds) {
|
||||||
|
this.state.selectedNodes = [nodeIds[0]]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
defaultSelectedNode: (_, { defaultSelectedNodes }) =>
|
||||||
|
defaultSelectedNodes !== undefined ? defaultSelectedNodes[0] : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ effects, state: { defaultSelectedNode, expandedNodes, selectedNodes }, collection }) => (
|
||||||
|
<TreeView
|
||||||
|
defaultCollapseIcon={<Icon icon='chevron-up' />}
|
||||||
|
defaultExpanded={[collection[0].id]}
|
||||||
|
defaultExpandIcon={<Icon icon='chevron-down' />}
|
||||||
|
expanded={expandedNodes}
|
||||||
|
multiSelect
|
||||||
|
onNodeSelect={effects.setSelectedNodeIds}
|
||||||
|
onNodeToggle={effects.setExpandedNodeIds}
|
||||||
|
selected={selectedNodes}
|
||||||
|
>
|
||||||
|
{collection.map(item => renderItem(item, defaultSelectedNode))}
|
||||||
|
</TreeView>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Tree
|
||||||
26
@xen-orchestra/lite/src/index.tsx
Normal file
26
@xen-orchestra/lite/src/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { Helmet } from 'react-helmet'
|
||||||
|
import { createGlobalStyle } from 'styled-components'
|
||||||
|
|
||||||
|
import App from './App/index'
|
||||||
|
|
||||||
|
const GlobalStyle = createGlobalStyle`
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, Verdana, Helvetica, Ubuntu, sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Helmet>
|
||||||
|
<link rel='shortcut icon' href='favicon.ico' />
|
||||||
|
</Helmet>
|
||||||
|
<GlobalStyle />
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
)
|
||||||
55
@xen-orchestra/lite/src/lang/en.json
Normal file
55
@xen-orchestra/lite/src/lang/en.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"about": "About",
|
||||||
|
"active": "Active",
|
||||||
|
"availableUpdates": "{nUpdates, number} available update{nUpdates, plural, one {} other {s}}",
|
||||||
|
"badCredentials": "Bad credentials",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"confirmCtrlAltDel": "Send Ctrl+Alt+Del to VM?",
|
||||||
|
"connect": "Connect",
|
||||||
|
"connectionError": "Connection error",
|
||||||
|
"consoleNotAvailable": "Console is only available for running VMs",
|
||||||
|
"ctrlAltDel": "Ctrl+Alt+Del",
|
||||||
|
"description": "Description",
|
||||||
|
"device": "Device",
|
||||||
|
"disconnect": "Disconnect",
|
||||||
|
"dns": "DNS",
|
||||||
|
"errorOccurred": "An error has occurred.",
|
||||||
|
"gateway": "Gateway",
|
||||||
|
"halted": "Halted",
|
||||||
|
"hosts": "Hosts",
|
||||||
|
"hostUnreachable": "Host unreachable",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"infrastructure": "Infrastructure",
|
||||||
|
"ip": "IP",
|
||||||
|
"loading": "Loading…",
|
||||||
|
"login": "Login",
|
||||||
|
"name": "Name",
|
||||||
|
"newFeaturesUnderConstruction": "New features are coming soon!",
|
||||||
|
"noHosts": "No hosts",
|
||||||
|
"noData": "No data",
|
||||||
|
"noImplemented": "Not implemented",
|
||||||
|
"noManagementPifs": "No management PIFs found",
|
||||||
|
"none": "None",
|
||||||
|
"noVms": "No VMs",
|
||||||
|
"notFound": "Not Found",
|
||||||
|
"pageNotFound": "This page doesn't exist.",
|
||||||
|
"xoLiteUnderConstruction": "XO Lite is under construction",
|
||||||
|
"noUpdatesAvailable": "No updates available",
|
||||||
|
"ok": "OK",
|
||||||
|
"password": "Password",
|
||||||
|
"paused": "Paused",
|
||||||
|
"reconnectionAttempt": "Trying to reconnect…",
|
||||||
|
"release": "Release",
|
||||||
|
"rememberMe": "Remember me",
|
||||||
|
"running": "Running",
|
||||||
|
"size": "Size",
|
||||||
|
"status": "Status",
|
||||||
|
"suspended": "Suspended",
|
||||||
|
"total": "Total",
|
||||||
|
"unreachableHost": "Click here to make sure your host ({name}) is reachable. You may have to allow self-signed SSL certificates in your browser.",
|
||||||
|
"vms": "VMs",
|
||||||
|
"version": "Version",
|
||||||
|
"versionValue": "Version {version}",
|
||||||
|
"vmStartLabel": "Start"
|
||||||
|
}
|
||||||
4
@xen-orchestra/lite/src/lang/fr.json
Normal file
4
@xen-orchestra/lite/src/lang/fr.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"connect": "Connexion",
|
||||||
|
"vmStartLabel": "Démarrer"
|
||||||
|
}
|
||||||
205
@xen-orchestra/lite/src/libs/xapi.ts
Normal file
205
@xen-orchestra/lite/src/libs/xapi.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import { EventEmitter } from 'events'
|
||||||
|
import { Map } from 'immutable'
|
||||||
|
import { Xapi } from 'xen-api'
|
||||||
|
|
||||||
|
export interface XapiObject {
|
||||||
|
$pool: Pool
|
||||||
|
$ref: string
|
||||||
|
$type: keyof types
|
||||||
|
$id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dictionary of XAPI types and their corresponding TypeScript types
|
||||||
|
interface types {
|
||||||
|
PIF: Pif
|
||||||
|
pool: Pool
|
||||||
|
VM: Vm
|
||||||
|
host: Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// XAPI types ---
|
||||||
|
|
||||||
|
export interface Pif extends XapiObject {
|
||||||
|
device: string
|
||||||
|
DNS: string
|
||||||
|
gateway: string
|
||||||
|
IP: string
|
||||||
|
management: boolean
|
||||||
|
network: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pool extends XapiObject {
|
||||||
|
name_label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PoolUpdate {
|
||||||
|
changelog: {
|
||||||
|
author: string
|
||||||
|
date: Date
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
description: string
|
||||||
|
license: string
|
||||||
|
name: string
|
||||||
|
release: string
|
||||||
|
size: number
|
||||||
|
url: string
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Vm extends XapiObject {
|
||||||
|
$consoles: Array<{ protocol: string; location: string }>
|
||||||
|
is_a_snapshot: boolean
|
||||||
|
is_a_template: boolean
|
||||||
|
is_control_domain: boolean
|
||||||
|
name_description: string
|
||||||
|
name_label: string
|
||||||
|
power_state: string
|
||||||
|
resident_on: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HostMetrics {
|
||||||
|
live: boolean
|
||||||
|
}
|
||||||
|
export interface Host extends XapiObject {
|
||||||
|
$metrics: HostMetrics
|
||||||
|
address: string
|
||||||
|
name_label: string
|
||||||
|
power_state: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------
|
||||||
|
|
||||||
|
export interface ObjectsByType extends Map<string, Map<string, XapiObject>> {
|
||||||
|
get<NSV, T extends keyof types>(key: T, notSetValue: NSV): Map<string, types[T]> | NSV
|
||||||
|
get<T extends keyof types>(key: T): Map<string, types[T]> | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class XapiConnection extends EventEmitter {
|
||||||
|
areObjectsFetched: Promise<void>
|
||||||
|
connected: boolean
|
||||||
|
objectsByType: ObjectsByType
|
||||||
|
sessionId?: string
|
||||||
|
|
||||||
|
_resolveObjectsFetched!: () => void
|
||||||
|
|
||||||
|
_xapi?: {
|
||||||
|
objects: EventEmitter & {
|
||||||
|
all: { [id: string]: XapiObject }
|
||||||
|
}
|
||||||
|
connect(): Promise<void>
|
||||||
|
disconnect(): Promise<void>
|
||||||
|
call: (method: string, ...args: unknown[]) => Promise<unknown>
|
||||||
|
_objectsFetched: Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.objectsByType = Map() as ObjectsByType
|
||||||
|
this.connected = false
|
||||||
|
this.areObjectsFetched = new Promise(resolve => {
|
||||||
|
this._resolveObjectsFetched = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async reattachSession(url: string): Promise<void> {
|
||||||
|
const sessionId = Cookies.get('sessionId')
|
||||||
|
if (sessionId === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.connect({ url, sessionId })
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect({
|
||||||
|
url,
|
||||||
|
user = 'root',
|
||||||
|
password,
|
||||||
|
sessionId,
|
||||||
|
rememberMe = Cookies.get('rememberMe') === 'true',
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
user?: string
|
||||||
|
password?: string
|
||||||
|
sessionId?: string
|
||||||
|
rememberMe?: boolean
|
||||||
|
}): Promise<void> {
|
||||||
|
const xapi = (this._xapi = new Xapi({
|
||||||
|
auth: { user, password, sessionId },
|
||||||
|
url,
|
||||||
|
watchEvents: true,
|
||||||
|
readonly: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const updateObjects = (objects: { [id: string]: XapiObject }) => {
|
||||||
|
try {
|
||||||
|
this.objectsByType = this.objectsByType.withMutations(objectsByType => {
|
||||||
|
Object.entries(objects).forEach(([id, object]) => {
|
||||||
|
if (object === undefined) {
|
||||||
|
// Remove
|
||||||
|
objectsByType.forEach((objects, type) => {
|
||||||
|
objectsByType.set(type, objects.remove(id))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Add or update
|
||||||
|
const { $type } = object
|
||||||
|
objectsByType.set($type, objectsByType.get($type, Map<string, XapiObject>()).set(id, object))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.emit('objects', this.objectsByType)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xapi.on('connected', () => {
|
||||||
|
this.sessionId = xapi.sessionId
|
||||||
|
this.connected = true
|
||||||
|
this.emit('connected')
|
||||||
|
})
|
||||||
|
|
||||||
|
xapi.on('disconnected', () => {
|
||||||
|
Cookies.remove('sessionId')
|
||||||
|
this.emit('disconnected')
|
||||||
|
})
|
||||||
|
|
||||||
|
xapi.on('sessionId', (sessionId: string) => {
|
||||||
|
if (rememberMe) {
|
||||||
|
Cookies.set('rememberMe', 'true', { expires: 7 })
|
||||||
|
}
|
||||||
|
Cookies.set('sessionId', sessionId, rememberMe ? { expires: 7 } : undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
await xapi.connect()
|
||||||
|
await xapi._objectsFetched
|
||||||
|
|
||||||
|
updateObjects(xapi.objects.all)
|
||||||
|
this._resolveObjectsFetched()
|
||||||
|
|
||||||
|
xapi.objects.on('add', updateObjects)
|
||||||
|
xapi.objects.on('update', updateObjects)
|
||||||
|
xapi.objects.on('remove', updateObjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): Promise<void> | undefined {
|
||||||
|
Cookies.remove('rememberMe')
|
||||||
|
Cookies.remove('sessionId')
|
||||||
|
const { _xapi } = this
|
||||||
|
if (_xapi !== undefined) {
|
||||||
|
return _xapi.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
call(method: string, ...args: unknown[]): Promise<unknown> {
|
||||||
|
const { _xapi, connected } = this
|
||||||
|
if (!connected || _xapi === undefined) {
|
||||||
|
throw new Error('Not connected to XAPI')
|
||||||
|
}
|
||||||
|
|
||||||
|
return _xapi.call(method, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
@xen-orchestra/lite/tsconfig.json
Normal file
63
@xen-orchestra/lite/tsconfig.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Basic Options */
|
||||||
|
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||||
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||||
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
"jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
"noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
"resolveJsonModule": true
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
}
|
||||||
|
}
|
||||||
6
@xen-orchestra/lite/types/decs.d.ts
vendored
Normal file
6
@xen-orchestra/lite/types/decs.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare module '@novnc/novnc/lib/rfb'
|
||||||
|
declare module 'human-format'
|
||||||
|
declare module 'iterable-backoff'
|
||||||
|
declare module 'json-rpc-protocol'
|
||||||
|
declare module 'promise-toolbox'
|
||||||
|
declare module 'xen-api'
|
||||||
42
@xen-orchestra/lite/types/reaclette.d.ts
vendored
Normal file
42
@xen-orchestra/lite/types/reaclette.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
type RenderParams<State, Props, Effects, Computed, ParentState, ParentEffects> = {
|
||||||
|
readonly effects: Effects & ParentEffects
|
||||||
|
readonly state: State & ParentState & Computed
|
||||||
|
readonly resetState: () => void
|
||||||
|
} & Props
|
||||||
|
|
||||||
|
interface EffectContext<State, Props, Effects, Computed, ParentState, ParentEffects> {
|
||||||
|
readonly effects: Effects & ParentEffects
|
||||||
|
readonly state: State & ParentState & Computed
|
||||||
|
readonly props: Props
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateSpec<State, Props, Effects, Computed, ParentState, ParentEffects> {
|
||||||
|
initialState?: State | ((props: Props) => State) // what about Reaclette's state inheritance?
|
||||||
|
effects?: {
|
||||||
|
initialize?: () => void | Promise<void>
|
||||||
|
finalize?: () => void | Promise<void>
|
||||||
|
} & Effects &
|
||||||
|
ThisType<EffectContext<State, Props, Effects, Computed, ParentState, ParentEffects>>
|
||||||
|
computed?: {
|
||||||
|
[ComputedName in keyof Computed]: (
|
||||||
|
state: State & ParentState & Computed,
|
||||||
|
props: Props
|
||||||
|
) => Computed[ComputedName] | Promise<Computed[ComputedName]>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'reaclette' {
|
||||||
|
function provideState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
stateSpec: StateSpec<State, Props, Effects, Computed, ParentState, ParentEffects>
|
||||||
|
): (component: React.Component<Props>) => React.Component<Props>
|
||||||
|
|
||||||
|
function injectState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
// FIXME: also accept class components
|
||||||
|
component: React.FC<RenderParams<State, Props, Effects, Computed, ParentState, ParentEffects>>
|
||||||
|
): React.ElementType<Props>
|
||||||
|
|
||||||
|
function withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
|
||||||
|
stateSpec: StateSpec<State, Props, Effects, Computed, ParentState, ParentEffects>,
|
||||||
|
component: React.FC<RenderParams<State, Props, Effects, Computed, ParentState, ParentEffects>>
|
||||||
|
): React.ElementType<Props>
|
||||||
|
}
|
||||||
21
@xen-orchestra/lite/types/theme.d.ts
vendored
Normal file
21
@xen-orchestra/lite/types/theme.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Theme as ThemeMui, ThemeOptions as ThemeOptionsMui } from '@mui/material/styles'
|
||||||
|
declare module '@mui/material/styles' {
|
||||||
|
// FIXME: when https://github.com/microsoft/TypeScript/issues/40315 is fixed.
|
||||||
|
// issue: Type 'Theme'/'ThemeOptions' recursively references itself as a base type.
|
||||||
|
interface Theme extends ThemeMui {
|
||||||
|
background: {
|
||||||
|
primary: {
|
||||||
|
dark: string
|
||||||
|
light: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface ThemeOptions extends ThemeOptionsMui {
|
||||||
|
background?: {
|
||||||
|
primary?: {
|
||||||
|
dark?: string
|
||||||
|
light?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
@xen-orchestra/lite/webpack.config.js
Normal file
72
@xen-orchestra/lite/webpack.config.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
|
||||||
|
const resolveApp = relative => path.resolve(__dirname, relative)
|
||||||
|
|
||||||
|
const { NODE_ENV = 'production' } = process.env
|
||||||
|
const __PROD__ = NODE_ENV === 'production'
|
||||||
|
|
||||||
|
// https://webpack.js.org/configuration/
|
||||||
|
module.exports = {
|
||||||
|
mode: NODE_ENV,
|
||||||
|
target: 'web',
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
},
|
||||||
|
entry: resolveApp('src/index.tsx'),
|
||||||
|
output: {
|
||||||
|
filename: __PROD__ ? '[name].[contenthash:8].js' : '[name].js',
|
||||||
|
path: resolveApp('dist'),
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
moduleIds: __PROD__ ? 'deterministic' : undefined,
|
||||||
|
runtimeChunk: true,
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
cacheDirectory: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: ['css-loader'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
dns: false,
|
||||||
|
},
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
},
|
||||||
|
devtool: __PROD__ ? 'source-map' : 'eval-cheap-module-source-map',
|
||||||
|
plugins: [
|
||||||
|
new (require('clean-webpack-plugin').CleanWebpackPlugin)(),
|
||||||
|
new (require('copy-webpack-plugin'))({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: resolveApp('public'),
|
||||||
|
to: resolveApp('dist'),
|
||||||
|
filter: file => file !== resolveApp('public/index.html'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new (require('html-webpack-plugin'))({
|
||||||
|
template: resolveApp('public/index.html'),
|
||||||
|
}),
|
||||||
|
new webpack.EnvironmentPlugin({ XAPI_HOST: '', NPM_VERSION: require('./package.json').version }),
|
||||||
|
new (require('node-polyfill-webpack-plugin'))(),
|
||||||
|
].filter(Boolean),
|
||||||
|
}
|
||||||
@@ -66,6 +66,10 @@ configure([
|
|||||||
// if filter is a string, then it is pattern
|
// if filter is a string, then it is pattern
|
||||||
// (https://github.com/visionmedia/debug#wildcards) which is
|
// (https://github.com/visionmedia/debug#wildcards) which is
|
||||||
// matched against the namespace of the logs
|
// matched against the namespace of the logs
|
||||||
|
//
|
||||||
|
// If it's an array, it will be handled as an array of filters
|
||||||
|
// and the transport will be used if any one of them match the
|
||||||
|
// current log
|
||||||
filter: process.env.DEBUG,
|
filter: process.env.DEBUG,
|
||||||
|
|
||||||
transport: transportConsole(),
|
transport: transportConsole(),
|
||||||
|
|||||||
@@ -4,6 +4,42 @@ const { compileGlobPattern } = require('./utils')
|
|||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
||||||
|
const compileFilter = filter => {
|
||||||
|
if (filter === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = typeof filter
|
||||||
|
if (type === 'function') {
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
if (type === 'string') {
|
||||||
|
const re = compileGlobPattern(filter)
|
||||||
|
return log => re.test(log.namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter)) {
|
||||||
|
const filters = filter.map(compileFilter).filter(_ => _ !== undefined)
|
||||||
|
const { length } = filters
|
||||||
|
if (length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (length === 1) {
|
||||||
|
return filters[0]
|
||||||
|
}
|
||||||
|
return log => {
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
if (filters[i](log)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError('unsupported `filter`')
|
||||||
|
}
|
||||||
|
|
||||||
const createTransport = config => {
|
const createTransport = config => {
|
||||||
if (typeof config === 'function') {
|
if (typeof config === 'function') {
|
||||||
return config
|
return config
|
||||||
@@ -19,26 +55,15 @@ const createTransport = config => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let { filter } = config
|
|
||||||
let transport = createTransport(config.transport)
|
|
||||||
const level = resolve(config.level)
|
const level = resolve(config.level)
|
||||||
|
const filter = compileFilter([config.filter, level === undefined ? undefined : log => log.level >= level])
|
||||||
|
|
||||||
|
let transport = createTransport(config.transport)
|
||||||
|
|
||||||
if (filter !== undefined) {
|
if (filter !== undefined) {
|
||||||
if (typeof filter === 'string') {
|
|
||||||
const re = compileGlobPattern(filter)
|
|
||||||
filter = log => re.test(log.namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
const orig = transport
|
const orig = transport
|
||||||
transport = function (log) {
|
transport = function (log) {
|
||||||
if ((level !== undefined && log.level >= level) || filter(log)) {
|
if (filter(log)) {
|
||||||
return orig.apply(this, arguments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (level !== undefined) {
|
|
||||||
const orig = transport
|
|
||||||
transport = function (log) {
|
|
||||||
if (log.level >= level) {
|
|
||||||
return orig.apply(this, arguments)
|
return orig.apply(this, arguments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": false,
|
"private": false,
|
||||||
"name": "@xen-orchestra/log",
|
"name": "@xen-orchestra/log",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "Logging system with decoupled producers/consumer",
|
"description": "Logging system with decoupled producers/consumer",
|
||||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
|
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
|
||||||
|
|||||||
@@ -20,36 +20,8 @@ if (process.stdout !== undefined && process.stdout.isTTY && process.stderr !== u
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NAMESPACE_COLORS = [
|
const NAMESPACE_COLORS = [
|
||||||
196,
|
196, 202, 208, 214, 220, 226, 190, 154, 118, 82, 46, 47, 48, 49, 50, 51, 45, 39, 33, 27, 21, 57, 93, 129, 165, 201,
|
||||||
202,
|
200, 199, 198, 197,
|
||||||
208,
|
|
||||||
214,
|
|
||||||
220,
|
|
||||||
226,
|
|
||||||
190,
|
|
||||||
154,
|
|
||||||
118,
|
|
||||||
82,
|
|
||||||
46,
|
|
||||||
47,
|
|
||||||
48,
|
|
||||||
49,
|
|
||||||
50,
|
|
||||||
51,
|
|
||||||
45,
|
|
||||||
39,
|
|
||||||
33,
|
|
||||||
27,
|
|
||||||
21,
|
|
||||||
57,
|
|
||||||
93,
|
|
||||||
129,
|
|
||||||
165,
|
|
||||||
201,
|
|
||||||
200,
|
|
||||||
199,
|
|
||||||
198,
|
|
||||||
197,
|
|
||||||
]
|
]
|
||||||
formatNamespace = namespace => {
|
formatNamespace = namespace => {
|
||||||
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const get = require('lodash/get')
|
const get = require('lodash/get')
|
||||||
const identity = require('lodash/identity')
|
const identity = require('lodash/identity')
|
||||||
|
const isEqual = require('lodash/isEqual')
|
||||||
const { createLogger } = require('@xen-orchestra/log')
|
const { createLogger } = require('@xen-orchestra/log')
|
||||||
const { parseDuration } = require('@vates/parse-duration')
|
const { parseDuration } = require('@vates/parse-duration')
|
||||||
const { watch } = require('app-conf')
|
const { watch } = require('app-conf')
|
||||||
@@ -48,7 +49,7 @@ module.exports = class Config {
|
|||||||
const watcher = config => {
|
const watcher = config => {
|
||||||
try {
|
try {
|
||||||
const value = processor(get(config, path))
|
const value = processor(get(config, path))
|
||||||
if (value !== prev) {
|
if (!isEqual(value, prev)) {
|
||||||
prev = value
|
prev = value
|
||||||
cb(value)
|
cb(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,14 @@
|
|||||||
"url": "https://vates.fr"
|
"url": "https://vates.fr"
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vates/parse-duration": "^0.1.1",
|
"@vates/parse-duration": "^0.1.1",
|
||||||
"@xen-orchestra/emit-async": "^0.0.0",
|
"@xen-orchestra/emit-async": "^0.1.0",
|
||||||
"@xen-orchestra/log": "^0.2.0",
|
"@xen-orchestra/log": "^0.3.0",
|
||||||
"app-conf": "^0.9.0",
|
"app-conf": "^0.9.0",
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export default {
|
|||||||
buffer.toString('hex', offset + 5, offset + 6),
|
buffer.toString('hex', offset + 5, offset + 6),
|
||||||
|
|
||||||
stringToEth: (string, buffer, offset) => {
|
stringToEth: (string, buffer, offset) => {
|
||||||
const eth = /^([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2})$/.exec(
|
const eth =
|
||||||
|
/^([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2})$/.exec(
|
||||||
string
|
string
|
||||||
)
|
)
|
||||||
assert(eth !== null)
|
assert(eth !== null)
|
||||||
@@ -50,7 +51,8 @@ export default {
|
|||||||
),
|
),
|
||||||
|
|
||||||
stringToip4: (string, buffer, offset) => {
|
stringToip4: (string, buffer, offset) => {
|
||||||
const ip = /^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/.exec(
|
const ip =
|
||||||
|
/^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/.exec(
|
||||||
string
|
string
|
||||||
)
|
)
|
||||||
assert(ip !== null)
|
assert(ip !== null)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
"cson-parser": "^4.0.7",
|
"cson-parser": "^4.0.7",
|
||||||
"getopts": "^2.2.3",
|
"getopts": "^2.2.3",
|
||||||
"http-request-plus": "^0.9.1",
|
"http-request-plus": "^0.12",
|
||||||
"json-rpc-protocol": "^0.13.1",
|
"json-rpc-protocol": "^0.13.1",
|
||||||
"promise-toolbox": "^0.19.2",
|
"promise-toolbox": "^0.19.2",
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
|
|||||||
@@ -36,7 +36,14 @@ async function main(argv) {
|
|||||||
|
|
||||||
const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {}
|
const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {}
|
||||||
|
|
||||||
const { _: args, file, help, host, raw, token } = getopts(argv, {
|
const {
|
||||||
|
_: args,
|
||||||
|
file,
|
||||||
|
help,
|
||||||
|
host,
|
||||||
|
raw,
|
||||||
|
token,
|
||||||
|
} = getopts(argv, {
|
||||||
alias: { file: 'f', help: 'h' },
|
alias: { file: 'f', help: 'h' },
|
||||||
boolean: ['help', 'raw'],
|
boolean: ['help', 'raw'],
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -93,10 +93,7 @@ declare namespace event {
|
|||||||
declare namespace backup {
|
declare namespace backup {
|
||||||
type SimpleIdPattern = { id: string | { __or: string[] } }
|
type SimpleIdPattern = { id: string | { __or: string[] } }
|
||||||
|
|
||||||
declare namespace backup {
|
interface BackupJob {
|
||||||
type SimpleIdPattern = { id: string | { __or: string[] } }
|
|
||||||
|
|
||||||
interface BackupJob {
|
|
||||||
id: string
|
id: string
|
||||||
type: 'backup'
|
type: 'backup'
|
||||||
compression?: 'native' | 'zstd' | ''
|
compression?: 'native' | 'zstd' | ''
|
||||||
@@ -146,13 +143,13 @@ declare namespace backup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listXoMetadataBackups(_: { remotes: { [id: string]: Remote } }): { [remoteId: string]: object[] }
|
function listXoMetadataBackups(_: { remotes: { [id: string]: Remote } }): { [remoteId: string]: object[] }
|
||||||
|
|
||||||
function run(_: {
|
function run(_: {
|
||||||
job: BackupJob | MetadataBackupJob
|
job: BackupJob | MetadataBackupJob
|
||||||
remotes: { [id: string]: Remote }
|
remotes: { [id: string]: Remote }
|
||||||
schedule: Schedule
|
schedule: Schedule
|
||||||
xapis?: { [id: string]: Xapi }
|
xapis?: { [id: string]: Xapi }
|
||||||
recordToXapi?: { [recordUuid: string]: string }
|
recordToXapi?: { [recordUuid: string]: string }
|
||||||
streamLogs: boolean = false
|
streamLogs: boolean = false
|
||||||
}): string
|
}): string
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "@xen-orchestra/proxy",
|
"name": "@xen-orchestra/proxy",
|
||||||
"version": "0.13.1",
|
"version": "0.14.7",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"description": "XO Proxy used to remotely execute backup jobs",
|
"description": "XO Proxy used to remotely execute backup jobs",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -18,9 +18,8 @@
|
|||||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||||
},
|
},
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"main": "dist/",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"xo-proxy": "dist/index.js"
|
"xo-proxy": "dist/index.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.13"
|
"node": ">=14.13"
|
||||||
@@ -29,20 +28,20 @@
|
|||||||
"@iarna/toml": "^2.2.0",
|
"@iarna/toml": "^2.2.0",
|
||||||
"@koa/router": "^10.0.0",
|
"@koa/router": "^10.0.0",
|
||||||
"@vates/compose": "^2.0.0",
|
"@vates/compose": "^2.0.0",
|
||||||
"@vates/decorate-with": "^0.0.1",
|
"@vates/decorate-with": "^0.1.0",
|
||||||
"@vates/disposable": "^0.1.1",
|
"@vates/disposable": "^0.1.1",
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/backups": "^0.11.0",
|
"@xen-orchestra/backups": "^0.13.0",
|
||||||
"@xen-orchestra/fs": "^0.17.0",
|
"@xen-orchestra/fs": "^0.18.0",
|
||||||
"@xen-orchestra/log": "^0.2.0",
|
"@xen-orchestra/log": "^0.3.0",
|
||||||
"@xen-orchestra/mixin": "^0.1.0",
|
"@xen-orchestra/mixin": "^0.1.0",
|
||||||
"@xen-orchestra/mixins": "^0.1.0",
|
"@xen-orchestra/mixins": "^0.1.1",
|
||||||
"@xen-orchestra/self-signed": "^0.1.0",
|
"@xen-orchestra/self-signed": "^0.1.0",
|
||||||
"@xen-orchestra/xapi": "^0.6.2",
|
"@xen-orchestra/xapi": "^0.7.0",
|
||||||
"ajv": "^8.0.3",
|
"ajv": "^8.0.3",
|
||||||
"app-conf": "^0.9.0",
|
"app-conf": "^0.9.0",
|
||||||
"async-iterator-to-stream": "^1.1.0",
|
"async-iterator-to-stream": "^1.1.0",
|
||||||
"fs-extra": "^9.1.0",
|
"fs-extra": "^10.0.0",
|
||||||
"get-stream": "^6.0.0",
|
"get-stream": "^6.0.0",
|
||||||
"getopts": "^2.2.3",
|
"getopts": "^2.2.3",
|
||||||
"golike-defer": "^0.5.1",
|
"golike-defer": "^0.5.1",
|
||||||
@@ -59,7 +58,7 @@
|
|||||||
"source-map-support": "^0.5.16",
|
"source-map-support": "^0.5.16",
|
||||||
"stoppable": "^1.0.6",
|
"stoppable": "^1.0.6",
|
||||||
"xdg-basedir": "^4.0.0",
|
"xdg-basedir": "^4.0.0",
|
||||||
"xen-api": "^0.32.0",
|
"xen-api": "^0.34.3",
|
||||||
"xo-common": "^0.7.0"
|
"xo-common": "^0.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
"@vates/toggle-scripts": "^1.0.0",
|
"@vates/toggle-scripts": "^1.0.0",
|
||||||
"babel-plugin-transform-dev": "^2.0.1",
|
"babel-plugin-transform-dev": "^2.0.1",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"index-modules": "^0.4.0"
|
"index-modules": "^0.4.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"_build": "index-modules --index-file index.mjs src/app/mixins && babel --delete-dir-on-start --keep-file-extension --source-maps --out-dir=dist/ src/",
|
"_build": "index-modules --index-file index.mjs src/app/mixins && babel --delete-dir-on-start --keep-file-extension --source-maps --out-dir=dist/ src/",
|
||||||
@@ -84,7 +83,7 @@
|
|||||||
"prepack": "toggle-scripts +postinstall +preuninstall",
|
"prepack": "toggle-scripts +postinstall +preuninstall",
|
||||||
"prepublishOnly": "yarn run build",
|
"prepublishOnly": "yarn run build",
|
||||||
"_preuninstall": "./scripts/systemd-service-installer",
|
"_preuninstall": "./scripts/systemd-service-installer",
|
||||||
"start": "./dist/index.js"
|
"start": "./dist/index.mjs"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Vates SAS",
|
"name": "Vates SAS",
|
||||||
|
|||||||
@@ -15,12 +15,23 @@ import { createLogger } from '@xen-orchestra/log'
|
|||||||
const { debug, warn } = createLogger('xo:proxy:api')
|
const { debug, warn } = createLogger('xo:proxy:api')
|
||||||
|
|
||||||
const ndJsonStream = asyncIteratorToStream(async function* (responseId, iterable) {
|
const ndJsonStream = asyncIteratorToStream(async function* (responseId, iterable) {
|
||||||
yield format.response(responseId, { $responseType: 'ndjson' }) + '\n'
|
let headerSent = false
|
||||||
|
try {
|
||||||
for await (const data of iterable) {
|
for await (const data of iterable) {
|
||||||
|
if (!headerSent) {
|
||||||
|
yield format.response(responseId, { $responseType: 'ndjson' }) + '\n'
|
||||||
|
headerSent = true
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
yield JSON.stringify(data) + '\n'
|
yield JSON.stringify(data) + '\n'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
warn('ndJsonStream', { error })
|
warn('ndJsonStream, item error', { error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
warn('ndJsonStream, fatal error', { error })
|
||||||
|
if (!headerSent) {
|
||||||
|
yield format.error(responseId, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { DurablePartition } from '@xen-orchestra/backups/DurablePartition.js'
|
|||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { formatVmBackups } from '@xen-orchestra/backups/formatVmBackups.js'
|
import { formatVmBackups } from '@xen-orchestra/backups/formatVmBackups.js'
|
||||||
import { ImportVmBackup } from '@xen-orchestra/backups/ImportVmBackup.js'
|
import { ImportVmBackup } from '@xen-orchestra/backups/ImportVmBackup.js'
|
||||||
|
import { JsonRpcError } from 'json-rpc-protocol'
|
||||||
import { Readable } from 'stream'
|
import { Readable } from 'stream'
|
||||||
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter.js'
|
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter.js'
|
||||||
import { RestoreMetadataBackup } from '@xen-orchestra/backups/RestoreMetadataBackup.js'
|
import { RestoreMetadataBackup } from '@xen-orchestra/backups/RestoreMetadataBackup.js'
|
||||||
@@ -108,7 +109,7 @@ export default class Backups {
|
|||||||
if (!__DEV__) {
|
if (!__DEV__) {
|
||||||
const license = await app.appliance.getSelfLicense()
|
const license = await app.appliance.getSelfLicense()
|
||||||
if (license === undefined) {
|
if (license === undefined) {
|
||||||
throw new Error('no valid proxy license')
|
throw new JsonRpcError('no valid proxy license')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return run.apply(this, arguments)
|
return run.apply(this, arguments)
|
||||||
@@ -187,6 +188,7 @@ export default class Backups {
|
|||||||
Task.run(
|
Task.run(
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
|
backupId,
|
||||||
jobId: metadata.jobId,
|
jobId: metadata.jobId,
|
||||||
srId: srUuid,
|
srId: srUuid,
|
||||||
time: metadata.timestamp,
|
time: metadata.timestamp,
|
||||||
|
|||||||
@@ -27,20 +27,18 @@
|
|||||||
"xo-upload-ova": "dist/index.js"
|
"xo-upload-ova": "dist/index.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"exec-promise": "^0.7.0",
|
"exec-promise": "^0.7.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"fs-promise": "^2.0.3",
|
|
||||||
"get-stream": "^6.0.0",
|
"get-stream": "^6.0.0",
|
||||||
"http-request-plus": "^0.8.0",
|
"http-request-plus": "^0.12",
|
||||||
"human-format": "^0.11.0",
|
"human-format": "^0.11.0",
|
||||||
"l33teral": "^3.0.3",
|
"l33teral": "^3.0.3",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"nice-pipe": "0.0.0",
|
|
||||||
"pretty-ms": "^7.0.0",
|
"pretty-ms": "^7.0.0",
|
||||||
"progress-stream": "^2.0.0",
|
"progress-stream": "^2.0.0",
|
||||||
"pw": "^0.0.4",
|
"pw": "^0.0.4",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import chalk from 'chalk'
|
|||||||
import execPromise from 'exec-promise'
|
import execPromise from 'exec-promise'
|
||||||
import FormData from 'form-data'
|
import FormData from 'form-data'
|
||||||
import { createReadStream } from 'fs'
|
import { createReadStream } from 'fs'
|
||||||
import { stat } from 'fs-promise'
|
import { stat } from 'fs-extra'
|
||||||
import getStream from 'get-stream'
|
import getStream from 'get-stream'
|
||||||
import hrp from 'http-request-plus'
|
import hrp from 'http-request-plus'
|
||||||
import humanFormat from 'human-format'
|
import humanFormat from 'human-format'
|
||||||
@@ -14,7 +14,6 @@ import l33t from 'l33teral'
|
|||||||
import isObject from 'lodash/isObject'
|
import isObject from 'lodash/isObject'
|
||||||
import getKeys from 'lodash/keys'
|
import getKeys from 'lodash/keys'
|
||||||
import startsWith from 'lodash/startsWith'
|
import startsWith from 'lodash/startsWith'
|
||||||
import nicePipe from 'nice-pipe'
|
|
||||||
import prettyMs from 'pretty-ms'
|
import prettyMs from 'pretty-ms'
|
||||||
import progressStream from 'progress-stream'
|
import progressStream from 'progress-stream'
|
||||||
import pw from 'pw'
|
import pw from 'pw'
|
||||||
@@ -22,10 +21,13 @@ import stripIndent from 'strip-indent'
|
|||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import Xo from 'xo-lib'
|
import Xo from 'xo-lib'
|
||||||
import { parseOVAFile } from 'xo-vmdk-to-vhd'
|
import { parseOVAFile } from 'xo-vmdk-to-vhd'
|
||||||
|
import { pipeline } from 'stream'
|
||||||
|
|
||||||
import pkg from '../package'
|
import pkg from '../package'
|
||||||
import { load as loadConfig, set as setConfig, unset as unsetConfig } from './config'
|
import { load as loadConfig, set as setConfig, unset as unsetConfig } from './config'
|
||||||
|
|
||||||
|
const noop = Function.prototype
|
||||||
|
|
||||||
function help() {
|
function help() {
|
||||||
return stripIndent(
|
return stripIndent(
|
||||||
`
|
`
|
||||||
@@ -206,7 +208,7 @@ export async function upload(args) {
|
|||||||
url = new URL(result[key], baseUrl)
|
url = new URL(result[key], baseUrl)
|
||||||
|
|
||||||
const { size: length } = await stat(file)
|
const { size: length } = await stat(file)
|
||||||
const input = nicePipe([
|
const input = pipeline(
|
||||||
createReadStream(file),
|
createReadStream(file),
|
||||||
progressStream(
|
progressStream(
|
||||||
{
|
{
|
||||||
@@ -215,7 +217,8 @@ export async function upload(args) {
|
|||||||
},
|
},
|
||||||
printProgress
|
printProgress
|
||||||
),
|
),
|
||||||
])
|
noop
|
||||||
|
)
|
||||||
formData.append('file', input, { filename: 'file', knownLength: length })
|
formData.append('file', input, { filename: 'file', knownLength: length })
|
||||||
try {
|
try {
|
||||||
return await hrp.post(url.toString(), { body: formData, headers: formData.getHeaders() }).readAll('utf-8')
|
return await hrp.post(url.toString(), { body: formData, headers: formData.getHeaders() }).readAll('utf-8')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xen-orchestra/xapi",
|
"name": "@xen-orchestra/xapi",
|
||||||
"version": "0.6.2",
|
"version": "0.7.0",
|
||||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
|
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
|
||||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"xo-common": "^0.7.0"
|
"xo-common": "^0.7.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"xen-api": "^0.32.0"
|
"xen-api": "^0.34.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
"prepublishOnly": "yarn run build"
|
"prepublishOnly": "yarn run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vates/decorate-with": "^0.0.1",
|
"@vates/decorate-with": "^0.1.0",
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/log": "^0.2.0",
|
"@xen-orchestra/log": "^0.3.0",
|
||||||
"d3-time-format": "^3.0.0",
|
"d3-time-format": "^3.0.0",
|
||||||
"golike-defer": "^0.5.1",
|
"golike-defer": "^0.5.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
|||||||
173
CHANGELOG.md
173
CHANGELOG.md
@@ -2,14 +2,181 @@
|
|||||||
|
|
||||||
## **next**
|
## **next**
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- [Backup] Go back to previous page instead of going to the overview after editing a job: keeps current filters and page (PR [#5913](https://github.com/vatesfr/xen-orchestra/pull/5913))
|
||||||
|
- [Health] Do not take into consideration duplicated MAC addresses from CR VMs (PR [#5916](https://github.com/vatesfr/xen-orchestra/pull/5916))
|
||||||
|
- [Health] Ability to filter duplicated MAC addresses by running VMs (PR [#5917](https://github.com/vatesfr/xen-orchestra/pull/5917))
|
||||||
|
- [Tables] Move the search bar and pagination to the top of the table (PR [#5914](https://github.com/vatesfr/xen-orchestra/pull/5914))
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- [SSH keys] Allow SSH key to be broken anywhere to avoid breaking page formatting (Thanks [@tstivers1990](https://github.com/tstivers1990)!) [#5891](https://github.com/vatesfr/xen-orchestra/issues/5891) (PR [#5892](https://github.com/vatesfr/xen-orchestra/pull/5892))
|
||||||
|
- [Netbox] Handle nested prefixes by always assigning an IP to the smallest prefix it matches (PR [#5908](https://github.com/vatesfr/xen-orchestra/pull/5908))
|
||||||
|
- [Netbox] Better handling and error messages when encountering issues due to UUID custom field not being configured correctly [#5905](https://github.com/vatesfr/xen-orchestra/issues/5905) [#5806](https://github.com/vatesfr/xen-orchestra/issues/5806) [#5834](https://github.com/vatesfr/xen-orchestra/issues/5834) (PR [#5909](https://github.com/vatesfr/xen-orchestra/pull/5909))
|
||||||
|
- [New VM] Don't send network config if untouched as all commented config can make Cloud-init fail [#5918](https://github.com/vatesfr/xen-orchestra/issues/5918) (PR [#5923](https://github.com/vatesfr/xen-orchestra/pull/5923))
|
||||||
|
|
||||||
### Released packages
|
### Released packages
|
||||||
|
|
||||||
- xen-api 0.32
|
- xen-api 0.34.3
|
||||||
|
- vhd-lib 1.2.0
|
||||||
|
- xo-server-netbox 0.3.1
|
||||||
|
- @xen-orchestra/proxy 0.14.7
|
||||||
|
- xo-server 5.82.3
|
||||||
|
- xo-web 5.88.0
|
||||||
|
|
||||||
## **5.58.1** (2021-05-06)
|
## **5.62.1** (2021-09-17)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- [VM/Advanced] Fix conversion from UEFI to BIOS boot firmware (PR [#5895](https://github.com/vatesfr/xen-orchestra/pull/5895))
|
||||||
|
- [VM/network] Support newline-delimited IP addresses reported by some guest tools
|
||||||
|
- Fix VM/host stats, VM creation with Cloud-init, and VM backups, with NATted hosts [#5896](https://github.com/vatesfr/xen-orchestra/issues/5896)
|
||||||
|
- [VM/import] Very small VMDK and OVA files were mangled upon import (PR [#5903](https://github.com/vatesfr/xen-orchestra/pull/5903))
|
||||||
|
|
||||||
|
### Released packages
|
||||||
|
|
||||||
|
- xen-api 0.34.2
|
||||||
|
- @xen-orchestra/proxy 0.14.6
|
||||||
|
- xo-server 5.82.2
|
||||||
|
|
||||||
|
## **5.62.0** (2021-08-31)
|
||||||
|
|
||||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- [Host] Add warning in case of unmaintained host version [#5840](https://github.com/vatesfr/xen-orchestra/issues/5840) (PR [#5847](https://github.com/vatesfr/xen-orchestra/pull/5847))
|
||||||
|
- [Backup] Use default migration network if set when importing/exporting VMs/VDIs (PR [#5883](https://github.com/vatesfr/xen-orchestra/pull/5883))
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- [New network] Ability for pool's admin to create a new network within the pool (PR [#5873](https://github.com/vatesfr/xen-orchestra/pull/5873))
|
||||||
|
- [Netbox] Synchronize primary IPv4 and IPv6 addresses [#5633](https://github.com/vatesfr/xen-orchestra/issues/5633) (PR [#5879](https://github.com/vatesfr/xen-orchestra/pull/5879))
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- [VM/network] Fix an issue where multiple IPs would be displayed in the same tag when using old Xen tools. This also fixes Netbox's IP synchronization for the affected VMs. (PR [#5860](https://github.com/vatesfr/xen-orchestra/pull/5860))
|
||||||
|
- [LDAP] Handle groups with no members (PR [#5862](https://github.com/vatesfr/xen-orchestra/pull/5862))
|
||||||
|
- Fix empty button on small size screen (PR [#5874](https://github.com/vatesfr/xen-orchestra/pull/5874))
|
||||||
|
- [Host] Fix `Cannot read property 'other_config' of undefined` error when enabling maintenance mode (PR [#5875](https://github.com/vatesfr/xen-orchestra/pull/5875))
|
||||||
|
|
||||||
|
### Released packages
|
||||||
|
|
||||||
|
- xen-api 0.34.1
|
||||||
|
- @xen-orchestra/xapi 0.7.0
|
||||||
|
- @xen-orchestra/backups 0.13.0
|
||||||
|
- @xen-orchestra/fs 0.18.0
|
||||||
|
- @xen-orchestra/log 0.3.0
|
||||||
|
- @xen-orchestra/mixins 0.1.1
|
||||||
|
- xo-server-auth-ldap 0.10.4
|
||||||
|
- xo-server-netbox 0.3.0
|
||||||
|
- xo-server 5.82.1
|
||||||
|
- xo-web 5.87.0
|
||||||
|
|
||||||
|
## **5.61.0** (2021-07-30)
|
||||||
|
|
||||||
|
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- [SR/disks] Display base copies' active VDIs (PR [#5826](https://github.com/vatesfr/xen-orchestra/pull/5826))
|
||||||
|
- [Netbox] Optionally allow self-signed certificates (PR [#5850](https://github.com/vatesfr/xen-orchestra/pull/5850))
|
||||||
|
- [Host] When supported, use pool's default migration network to evacuate host [#5802](https://github.com/vatesfr/xen-orchestra/issues/5802) (PR [#5851](https://github.com/vatesfr/xen-orchestra/pull/5851))
|
||||||
|
- [VM] shutdown/reboot: offer to force shutdown/reboot the VM if no Xen tools were detected [#5838](https://github.com/vatesfr/xen-orchestra/issues/5838) (PR [#5855](https://github.com/vatesfr/xen-orchestra/pull/5855))
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- [Netbox] Add information about a failed request to the error log to help better understand what happened [#5834](https://github.com/vatesfr/xen-orchestra/issues/5834) (PR [#5842](https://github.com/vatesfr/xen-orchestra/pull/5842))
|
||||||
|
- [VM/console] Ability to rescan ISO SRs (PR [#5841](https://github.com/vatesfr/xen-orchestra/pull/5841))
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- [VM/disks] Fix `an error has occured` when self service user was on VM disk view (PR [#5841](https://github.com/vatesfr/xen-orchestra/pull/5841))
|
||||||
|
- [Backup] Protect replicated VMs from being started on specific hosts (PR [#5852](https://github.com/vatesfr/xen-orchestra/pull/5852))
|
||||||
|
|
||||||
|
### Released packages
|
||||||
|
|
||||||
|
- @xen-orchestra/backups 0.12.2
|
||||||
|
- @xen-orchestra/proxy 0.14.4
|
||||||
|
- xo-server-netbox 0.2.0
|
||||||
|
- xo-web 5.86.0
|
||||||
|
- xo-server 5.81.2
|
||||||
|
|
||||||
|
## **5.60.0** (2021-06-30)
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- [VM/disks] Ability to rescan ISO SRs (PR [#5814](https://github.com/vatesfr/xen-orchestra/pull/5814))
|
||||||
|
- [VM/snapshots] Identify VM's current snapshot with an icon next to the snapshot's name (PR [#5824](https://github.com/vatesfr/xen-orchestra/pull/5824))
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- [OVA import] improve OVA import error reporting (PR [#5797](https://github.com/vatesfr/xen-orchestra/pull/5797))
|
||||||
|
- [Backup] Distinguish error messages between cancelation and interrupted HTTP connection
|
||||||
|
- [Jobs] Add `host.emergencyShutdownHost` to the list of methods that jobs can call (PR [#5818](https://github.com/vatesfr/xen-orchestra/pull/5818))
|
||||||
|
- [Host/Load-balancer] Log VM and host names when a VM is migrated + category (density, performance, ...) (PR [#5808](https://github.com/vatesfr/xen-orchestra/pull/5808))
|
||||||
|
- [VM/new disk] Auto-fill disk name input with generated unique name (PR [#5828](https://github.com/vatesfr/xen-orchestra/pull/5828))
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- [IPs] Handle space-delimited IP address format provided by outdated guest tools [5801](https://github.com/vatesfr/xen-orchestra/issues/5801) (PR [5805](https://github.com/vatesfr/xen-orchestra/pull/5805))
|
||||||
|
- [API/pool.listPoolsMatchingCriteria] fix `unknown error from the peer` error (PR [5807](https://github.com/vatesfr/xen-orchestra/pull/5807))
|
||||||
|
- [Backup] Limit number of connections to hosts, which should reduce the occurences of `ECONNRESET`
|
||||||
|
- [Plugins/perf-alert] All mode: only selects running hosts and VMs (PR [5811](https://github.com/vatesfr/xen-orchestra/pull/5811))
|
||||||
|
- [New VM] Fix summary section always showing "0 B" for RAM (PR [#5817](https://github.com/vatesfr/xen-orchestra/pull/5817))
|
||||||
|
- [Backup/Restore] Fix _start VM after restore_ [5820](https://github.com/vatesfr/xen-orchestra/issues/5820)
|
||||||
|
- [Netbox] Fix a bug where some devices' IPs would get deleted from Netbox (PR [#5821](https://github.com/vatesfr/xen-orchestra/pull/5821))
|
||||||
|
- [Netbox] Fix an issue where some IPv6 would be deleted just to be immediately created again (PR [#5822](https://github.com/vatesfr/xen-orchestra/pull/5822))
|
||||||
|
|
||||||
|
### Released packages
|
||||||
|
|
||||||
|
- @vates/decorate-with 0.1.0
|
||||||
|
- xen-api 0.33.1
|
||||||
|
- @xen-orchestra/xapi 0.6.4
|
||||||
|
- @xen-orchestra/backups 0.12.0
|
||||||
|
- @xen-orchestra/proxy 0.14.3
|
||||||
|
- vhd-lib 1.1.0
|
||||||
|
- vhd-cli 0.4.0
|
||||||
|
- xo-server-netbox 0.1.2
|
||||||
|
- xo-server-perf-alert 0.3.2
|
||||||
|
- xo-server-load-balancer 0.7.0
|
||||||
|
- xo-server 5.80.0
|
||||||
|
- xo-web 5.84.0
|
||||||
|
|
||||||
|
## **5.59.0** (2021-05-31)
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- [Smart backup] Report missing pools [#2844](https://github.com/vatesfr/xen-orchestra/issues/2844) (PR [#5768](https://github.com/vatesfr/xen-orchestra/pull/5768))
|
||||||
|
- [Metadata Backup] Add a warning on restoring a metadata backup (PR [#5769](https://github.com/vatesfr/xen-orchestra/pull/5769))
|
||||||
|
- [Netbox] [Plugin](https://xen-orchestra.com/docs/advanced.html#netbox) to synchronize pools, VMs and IPs with [Netbox](https://netbox.readthedocs.io/en/stable/) (PR [#5783](https://github.com/vatesfr/xen-orchestra/pull/5783))
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- [SAML] Compatible with users created with other authentication providers (PR [#5781](https://github.com/vatesfr/xen-orchestra/pull/5781))
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- [SDN Controller] Private network creation failure when the tunnels were created on different devices [Forum #4620](https://xcp-ng.org/forum/topic/4620/no-pif-found-in-center) (PR [#5793](https://github.com/vatesfr/xen-orchestra/pull/5793))
|
||||||
|
|
||||||
|
### Released packages
|
||||||
|
|
||||||
|
- @xen-orchestra/emit-async 0.1.0
|
||||||
|
- @xen-orchestra/defined 0.0.1
|
||||||
|
- xo-collection 0.5.0
|
||||||
|
- @xen-orchestra/log 0.2.1
|
||||||
|
- xen-api 0.33.0
|
||||||
|
- @xen-orchestra/xapi 0.6.3
|
||||||
|
- xo-server-auth-saml 0.9.0
|
||||||
|
- xo-server-backup-reports 0.16.10
|
||||||
|
- xo-server-netbox 0.1.1
|
||||||
|
- xo-server-sdn-controller 1.0.5
|
||||||
|
- xo-web 5.82.0
|
||||||
|
- xo-server 5.79.5
|
||||||
|
|
||||||
|
## **5.58.1** (2021-05-06)
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
- [Backups] Better handling of errors in remotes, fix `task has already ended`
|
- [Backups] Better handling of errors in remotes, fix `task has already ended`
|
||||||
@@ -64,8 +231,6 @@
|
|||||||
|
|
||||||
## **5.57.1** (2021-04-13)
|
## **5.57.1** (2021-04-13)
|
||||||
|
|
||||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
- [Host/Load-balancer] Add option to disable migration (PR [#5706](https://github.com/vatesfr/xen-orchestra/pull/5706))
|
- [Host/Load-balancer] Add option to disable migration (PR [#5706](https://github.com/vatesfr/xen-orchestra/pull/5706))
|
||||||
|
|||||||
@@ -7,15 +7,10 @@
|
|||||||
|
|
||||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||||
|
|
||||||
- [Metadata Backup] Add a warning on restoring a metadata backup (PR [#5769](https://github.com/vatesfr/xen-orchestra/pull/5769))
|
|
||||||
- [SAML] Compatible with users created with other authentication providers (PR [#5781](https://github.com/vatesfr/xen-orchestra/pull/5781))
|
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||||
|
|
||||||
- [Smart backup] Report missing pools [#2844](https://github.com/vatesfr/xen-orchestra/issues/2844) (PR [#5768](https://github.com/vatesfr/xen-orchestra/pull/5768))
|
|
||||||
|
|
||||||
### Packages to release
|
### Packages to release
|
||||||
|
|
||||||
> Packages will be released in the order they are here, therefore, they should
|
> Packages will be released in the order they are here, therefore, they should
|
||||||
@@ -32,13 +27,3 @@
|
|||||||
> - major: if the change breaks compatibility
|
> - major: if the change breaks compatibility
|
||||||
>
|
>
|
||||||
> In case of conflict, the highest (lowest in previous list) `$version` wins.
|
> In case of conflict, the highest (lowest in previous list) `$version` wins.
|
||||||
|
|
||||||
- @xen-orchestra/emit-async minor
|
|
||||||
- @xen-orchestra/defined patch
|
|
||||||
- xo-collection minor
|
|
||||||
- @xen-orchestra/log patch
|
|
||||||
- xen-api minor
|
|
||||||
- xo-server-auth-saml minor
|
|
||||||
- xo-server-backup-reports patch
|
|
||||||
- xo-web minor
|
|
||||||
- xo-server patch
|
|
||||||
|
|||||||
@@ -114,17 +114,18 @@ We need your feedback on this feature!
|
|||||||
|
|
||||||
The plugin "web-hooks" needs to be installed and loaded for this feature to work.
|
The plugin "web-hooks" needs to be installed and loaded for this feature to work.
|
||||||
|
|
||||||
You can trigger an HTTP POST request to a URL when a Xen Orchestra API method is called.
|
You can trigger an HTTP POST request to a URL when a Xen Orchestra API method is called or when a backup job runs.
|
||||||
|
|
||||||
- Go to Settings > Plugins > Web hooks
|
- Go to Settings > Plugins > Web hooks
|
||||||
- Add new hooks
|
- Add new hooks
|
||||||
- For each hook, configure:
|
- For each hook, configure:
|
||||||
- Method: the XO API method that will trigger the HTTP request when called
|
- Method: the XO API method that will trigger the HTTP request when called. For backup jobs, choose `backupNg.runJob`.
|
||||||
- Type:
|
- Type:
|
||||||
- pre: the request will be sent when the method is called
|
- pre: the request will be sent when the method is called
|
||||||
- post: the request will be sent after the method action is completed
|
- post: the request will be sent after the method action is completed
|
||||||
- pre/post: both
|
- pre/post: both
|
||||||
- URL: the full URL which the requests will be sent to
|
- URL: the full URL which the requests will be sent to
|
||||||
|
- Wait for response: you can choose to wait for the web hook response before the method is actually called ("pre" hooks only). This can be useful if you need to automatically run some tasks before a certain method is called.
|
||||||
- Save the plugin configuration
|
- Save the plugin configuration
|
||||||
|
|
||||||
From now on, a request will be sent to the corresponding URLs when a configured method is called by an XO client.
|
From now on, a request will be sent to the corresponding URLs when a configured method is called by an XO client.
|
||||||
@@ -320,6 +321,40 @@ It works with few steps:
|
|||||||
|
|
||||||
From there, you can even manage your existing resources with Terraform!
|
From there, you can even manage your existing resources with Terraform!
|
||||||
|
|
||||||
|
## Netbox
|
||||||
|
|
||||||
|
Synchronize your pools, VMs, network interfaces and IP addresses with your [Netbox](https://netbox.readthedocs.io/en/stable/) instance.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Go to your Netbox interface
|
||||||
|
- Configure prefixes:
|
||||||
|
- Go to IPAM > Prefixes > Add
|
||||||
|
- Manually create as many prefixes as needed for your infrastructure's IP addresses
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
XO will try to find the right prefix for each IP address. If it can't find a prefix that fits, the IP address won't be synchronized.
|
||||||
|
:::
|
||||||
|
|
||||||
|
- Generate a token:
|
||||||
|
- Go to Admin > Tokens > Add token
|
||||||
|
- Create a token with "Write enabled"
|
||||||
|
- Add a UUID custom field:
|
||||||
|
- Got to Admin > Custom fields > Add custom field
|
||||||
|
- Create a custom field called "uuid" (lower case!)
|
||||||
|
- Assign it to object types `virtualization > cluster` and `virtualization > virtual machine`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Go to Xen Orchestra > Settings > Plugins > Netbox and fill out the configuration:
|
||||||
|
- Endpoint: the URL of your Netbox instance (e.g.: `https://netbox.company.net`)
|
||||||
|
- Unauthorized certificate: only for HTTPS, enable this option if your Netbox instance uses a self-signed SSL certificate
|
||||||
|
- Token: the token you generated earlier
|
||||||
|
- Pools: the pools you wish to automatically synchronize with Netbox
|
||||||
|
- Interval: the time interval (in hours) between 2 auto-synchronizations. Leave empty if you don't want to synchronize automatically.
|
||||||
|
- Load the plugin (button next to the plugin's name)
|
||||||
|
- Manual synchronization: if you correctly configured and loaded the plugin, a "Synchronize with Netbox" button will appear in every pool's Advanced tab, which allows you to manually synchronize it with Netbox
|
||||||
|
|
||||||
## Recipes
|
## Recipes
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|||||||
BIN
docs/assets/antiaffinity.png
Normal file
BIN
docs/assets/antiaffinity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/assets/customfield.png
Normal file
BIN
docs/assets/customfield.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
docs/assets/netbox.png
Normal file
BIN
docs/assets/netbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
@@ -87,3 +87,7 @@ You need to be an admin:
|
|||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Web hooks
|
||||||
|
|
||||||
|
You can also configure web hooks to be sent to a custom server before and/or after a backup job runs. This won't send a formatted report but raw JSON data that you can use in custom scripts on your side. Follow the [web-hooks plugin documentation](./advanced.html#web-hooks) to configure it.
|
||||||
|
|||||||
@@ -283,39 +283,40 @@ When it's done exporting, we'll remove the snapshot. Note: this operation will t
|
|||||||
|
|
||||||
### Concurrency
|
### Concurrency
|
||||||
|
|
||||||
|
Concurrency is a parameter that let you define how many VMs your backup job will manage simultaneously.
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
- Default concurrency value is 2 if left empty.
|
||||||
|
:::
|
||||||
|
|
||||||
Let's say you want to backup 50 VMs (each with 1x disk) at 3:00 AM. There are **2 different strategies**:
|
Let's say you want to backup 50 VMs (each with 1x disk) at 3:00 AM. There are **2 different strategies**:
|
||||||
|
|
||||||
1. backup VM #1 (snapshot, export, delete snapshots) **then** backup VM #2 -> _fully sequential strategy_
|
1. backup VM #1 (snapshot, export, delete snapshots) **then** backup VM #2 -> _fully sequential strategy_
|
||||||
2. snapshot all VMs, **then** export all snapshots, **then** delete all snapshots for finished exports -> _fully parallel strategy_
|
2. snapshot all VMs, **then** export all snapshots, **then** delete all snapshots for finished exports -> _fully parallel strategy_
|
||||||
|
|
||||||
The first purely sequential strategy will lead to a big problem: **you can't predict when a snapshot of your data will occur**. Because you can't predict the first VM export time (let's say 3 hours), then your second VM will have its snapshot taken 3 hours later, at 6 AM. We assume that's not what you meant when you specified "backup everything at 3 AM". You would end up with data from 6 AM (and later) for other VMs.
|
The first purely sequential strategy will lead to the fact that: **you can't predict when a snapshot of your data will occur**. Because you can't predict the first VM export time (let's say 3 hours), then your second VM will have its snapshot taken 3 hours later, at 6 AM.
|
||||||
|
|
||||||
Strategy number 2 is better in this aspect: all the snapshots will be taken at 3 AM. However **it's risky without limits**: it means potentially doing 50 snapshots or more at once on the same storage. **Since XenServer doesn't have a queue**, it will try to do all of them at once. This is also prone to race conditions and could cause crashes on your storage.
|
|
||||||
|
|
||||||
So what's the best choice? Continue below to learn how to best configure concurrency for your needs.
|
|
||||||
|
|
||||||
#### Best choice
|
|
||||||
|
|
||||||
By default the _parallel strategy_ is, on paper, the most logical one. But we need to give it some limits on concurrency.
|
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
Xen Orchestra can be connected to multiple pools at once. So the concurrency number applies **per pool**.
|
If you need your backup to be done at a specific time you should consider creating a specific backup task for this VM.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Each step has its own concurrency to fit its requirements:
|
Strategy number 2 is to parallelise: all the snapshots will be taken at 3 AM. However **it's risky without limits**: it means potentially doing 50 snapshots or more at once on the same storage. **Since XenServer doesn't have a queue**, it will try to do all of them at once. This is also prone to race conditions and could cause crashes on your storage.
|
||||||
|
|
||||||
- **snapshot process** needs to be performed with the lowest concurrency possible. 2 is a good compromise: one snapshot is fast, but a stuck snapshot won't block the whole job. That's why a concurrency of 2 is not too bad on your storage. Basically, at 3 AM, we'll do all the VM snapshots needed, 2 at a time.
|
By default the _parallel strategy_ is, on paper, the most logical one. But you need to be careful and give it some limits on concurrency.
|
||||||
- **disk export process** is bottlenecked by XCP-ng/XenServer - so to get the most of it, you can use up to 12 in parallel. As soon a snapshot is done, the export process will start, until reaching 12 at once. Then as soon as one in those 12 is finished, another one will appear until there is nothing more to export.
|
|
||||||
- **VM export process:** the 12 disk export limit mentioned above applies to VDI exports, which happen during delta exports. For full VM exports (for example, for full backup job types), there is a built in limit of 2. This means if you have a full backup job of 6 VMs, only 2 will be exported at once.
|
|
||||||
- **snapshot deletion** can't happen all at once because the previous step durations are random - no need to implement concurrency on this one.
|
|
||||||
|
|
||||||
This is how it currently works in Xen Orchestra. But sometimes, you also want to have _sequential_ backups combined with the _parallel strategy_. That's why we introduced a sequential option in the advanced section of backup-ng:
|
:::danger
|
||||||
|
High concurrency could impact your dom0 and network performances.
|
||||||
:::tip
|
|
||||||
0 means it will be fully **parallel** for all VMs.
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
If you job contains 50 VMs for example, you could specify a sequential backup with a limit of "25 at once" (enter 25 in the concurrency field). This means at 3 AM, we'll do 25 snapshots (2 at a time), then exports. As soon as the first VM backup is completely finished (snapshot removed), then we'll start the 26th and so on, to always keep a max of 25x VM backups going in parallel.
|
You should be aware of your hardware limitation when defining the best concurrency for your XCP-ng infrastructure, never put concurrency too high or you could impact your VMs performances.
|
||||||
|
The best way to define the best concurrency for you is by increasing it slowly and watching the result on backup time.
|
||||||
|
|
||||||
|
So to summarize, if you set your concurrency at 6 and you have 20 Vms to backup the process will be the following:
|
||||||
|
- We start the backup of the first 6 VMs.
|
||||||
|
- When one VM backup as ended we will launch the next VM backup.
|
||||||
|
- We're keep launching new VM backup until the 20 VMs are finished, keeping 6 backups running.
|
||||||
|
|
||||||
|
Removing the snapshot will trigger the coalesce process for the first VM, this is an automated action not triggered directly by the backup job.
|
||||||
|
|
||||||
## Backup modifier tags
|
## Backup modifier tags
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ As you may have seen in other parts of the documentation, XO is composed of two
|
|||||||
|
|
||||||
### NodeJS
|
### NodeJS
|
||||||
|
|
||||||
XO needs Node.js. **Please use Node LTS version 14**.
|
XO needs Node.js. **Please always use latest Node LTS**.
|
||||||
|
|
||||||
We'll consider at this point that you've got a working node on your box. E.g:
|
We'll consider at this point that you've got a working node on your box. E.g:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ node -v
|
$ node -v
|
||||||
v14.16.0
|
v14.17.0
|
||||||
```
|
```
|
||||||
|
|
||||||
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.
|
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.
|
||||||
@@ -46,7 +46,7 @@ apt-get install build-essential redis-server libpng-dev git python-minimal libvh
|
|||||||
You need to use the `git` source code manager to fetch the code. Ideally, you should run XO as a non-root user, and if you choose to, you need to set up `sudo` to be able to mount NFS remotes. As your chosen non-root (or root) user, run the following:
|
You need to use the `git` source code manager to fetch the code. Ideally, you should run XO as a non-root user, and if you choose to, you need to set up `sudo` to be able to mount NFS remotes. As your chosen non-root (or root) user, run the following:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone -b master http://github.com/vatesfr/xen-orchestra
|
git clone -b master https://github.com/vatesfr/xen-orchestra
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: xo-server and xo-web have been migrated to the [xen-orchestra](https://github.com/vatesfr/xen-orchestra) mono-repository - so you only need the single clone command above
|
> Note: xo-server and xo-web have been migrated to the [xen-orchestra](https://github.com/vatesfr/xen-orchestra) mono-repository - so you only need the single clone command above
|
||||||
@@ -122,9 +122,9 @@ forever start dist/cli.mjs
|
|||||||
yarn global add forever
|
yarn global add forever
|
||||||
yarn global add forever-service
|
yarn global add forever-service
|
||||||
# Be sure to edit the path below to where your install is located!
|
# Be sure to edit the path below to where your install is located!
|
||||||
cd /home/username/xen-orchestra/packages/xo-server/bin/
|
cd /home/username/xen-orchestra/packages/xo-server/
|
||||||
# Change the username below to the user owning XO
|
# Change the username below to the user owning XO
|
||||||
forever-service install orchestra -r username -s xo-server
|
forever-service install orchestra -r username -s dist/cli.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
The forever-service command above must be run in the xo-server bin directory. Now you can manage the service, and it will start on boot with the machine:
|
The forever-service command above must be run in the xo-server bin directory. Now you can manage the service, and it will start on boot with the machine:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Full backups
|
# Full backups
|
||||||
|
|
||||||
You can schedule full backups of your VMs, by exporting them to the local XOA file-system, or directly to an NFS or SMB share. The "rentention" parameter allows you to modify how many backups are retained (by removing the oldest one).
|
You can schedule full backups of your VMs, by exporting them to the local XOA file-system, or directly to an NFS or SMB share. The "retention" parameter allows you to modify how many backups are retained (by removing the oldest one).
|
||||||
|
|
||||||
[](https://xen-orchestra.com/blog/backup-your-xenserver-vms-with-xen-orchestra/)
|
[](https://xen-orchestra.com/blog/backup-your-xenserver-vms-with-xen-orchestra/)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user