Compare commits
304 Commits
gab-docker
...
persian-lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2930d7e2b | ||
|
|
fa881fa0f4 | ||
|
|
d1172b65e7 | ||
|
|
ff383607ec | ||
|
|
769e27e2cb | ||
|
|
8ec5461338 | ||
|
|
4a2843cb67 | ||
|
|
a0e69a79ab | ||
|
|
3da94f18df | ||
|
|
17cb59b898 | ||
|
|
315e5c9289 | ||
|
|
01ba10fedb | ||
|
|
13e7594560 | ||
|
|
f9ac2ac84d | ||
|
|
09cfac1111 | ||
|
|
008f7a30fd | ||
|
|
ff65dbcba7 | ||
|
|
264a0d1678 | ||
|
|
7dcaf454ed | ||
|
|
17b2756291 | ||
|
|
57e48b5d34 | ||
|
|
57ed984e5a | ||
|
|
100122f388 | ||
|
|
12d4b3396e | ||
|
|
ab35c710cb | ||
|
|
4bd5b38aeb | ||
|
|
836db1b807 | ||
|
|
73d88cc5f1 | ||
|
|
3def66d968 | ||
|
|
3f73138fc3 | ||
|
|
bfe621a21d | ||
|
|
32fa792eeb | ||
|
|
a833050fc2 | ||
|
|
e7e6294bc3 | ||
|
|
7c71884e27 | ||
|
|
3e822044f2 | ||
|
|
d457f5fca4 | ||
|
|
1837e01719 | ||
|
|
f17f5abf0f | ||
|
|
82c229c755 | ||
|
|
c7e3ba3184 | ||
|
|
470c9bb6c8 | ||
|
|
bb3ab20b2a | ||
|
|
90ce1c4d1e | ||
|
|
5c436f3870 | ||
|
|
159339625d | ||
|
|
87e6f7fded | ||
|
|
fd2c7c2fc3 | ||
|
|
7fc76c1df4 | ||
|
|
f2758d036d | ||
|
|
ac670da793 | ||
|
|
c0465eb4d9 | ||
|
|
cea55b03e5 | ||
|
|
d78d802066 | ||
|
|
a562c74492 | ||
|
|
d1f2e0a84b | ||
|
|
49e2d128ad | ||
|
|
f587798fb0 | ||
|
|
3430ee743b | ||
|
|
83299587b0 | ||
|
|
7c0ecf9b06 | ||
|
|
abfd84d32c | ||
|
|
0583a978be | ||
|
|
75989cf92d | ||
|
|
f1cc284b6f | ||
|
|
0444cf0b3b | ||
|
|
226f9ad964 | ||
|
|
a956cb2ac9 | ||
|
|
76a91cc5e9 | ||
|
|
f012d126b9 | ||
|
|
bae0b52893 | ||
|
|
a24512cea9 | ||
|
|
84b75e8a58 | ||
|
|
6e25b7a83a | ||
|
|
136718df7e | ||
|
|
d48ef1f810 | ||
|
|
9e60c53750 | ||
|
|
f3c5e817a3 | ||
|
|
60f6e54da1 | ||
|
|
f5a59caca2 | ||
|
|
6ea671a434 | ||
|
|
036f3f6bd0 | ||
|
|
12552a1391 | ||
|
|
e9b658b60d | ||
|
|
15f69a19f5 | ||
|
|
54d885fa9c | ||
|
|
11cc299940 | ||
|
|
091b0a3ef3 | ||
|
|
87874a4b81 | ||
|
|
86aaa50946 | ||
|
|
68b2c287eb | ||
|
|
61f1316c42 | ||
|
|
afadc8f95a | ||
|
|
955ef6806c | ||
|
|
4d55c5ae48 | ||
|
|
5c6ae1912b | ||
|
|
083483645e | ||
|
|
c077e9a699 | ||
|
|
280b60808f | ||
|
|
eb9608b893 | ||
|
|
a29f3d67ea | ||
|
|
6b150dc8a8 | ||
|
|
8f55884602 | ||
|
|
2fdba2eb0f | ||
|
|
7e4bd30f04 | ||
|
|
eb8f098aaf | ||
|
|
5237fdd387 | ||
|
|
8a07b7a3db | ||
|
|
a41037833c | ||
|
|
6a780d94a3 | ||
|
|
506ef0b44f | ||
|
|
a4d1d41b6a | ||
|
|
4e9477f34a | ||
|
|
43b6285437 | ||
|
|
c26a7a3e51 | ||
|
|
93eb42785d | ||
|
|
02bb622e92 | ||
|
|
b873c147a6 | ||
|
|
5e7fb7a881 | ||
|
|
97790313eb | ||
|
|
954b29cb61 | ||
|
|
dc6a13962f | ||
|
|
23da202790 | ||
|
|
f237101b4a | ||
|
|
8a99326a76 | ||
|
|
8c95974e65 | ||
|
|
3f7454efad | ||
|
|
e5c890e29b | ||
|
|
53e0f17c55 | ||
|
|
34f6be868e | ||
|
|
c84b899276 | ||
|
|
266a26fa31 | ||
|
|
bbf92be652 | ||
|
|
e19c7b949d | ||
|
|
5ce6f1fe4d | ||
|
|
9c36520c79 | ||
|
|
a85a8ea208 | ||
|
|
c2e0c97d94 | ||
|
|
a5447fda3c | ||
|
|
507e9a55c2 | ||
|
|
5bd0eb3362 | ||
|
|
458496a09e | ||
|
|
f13a98b6b8 | ||
|
|
dde32724b1 | ||
|
|
63b76fdb50 | ||
|
|
1b9cd56e9f | ||
|
|
784b0dded8 | ||
|
|
4a658787de | ||
|
|
4beb49041d | ||
|
|
1f6e29084f | ||
|
|
7c6cb2454b | ||
|
|
96720d186c | ||
|
|
a45fb88c48 | ||
|
|
b4b0a925af | ||
|
|
72822c9529 | ||
|
|
ca6cdbf9cf | ||
|
|
74cd35f527 | ||
|
|
010866a0ef | ||
|
|
5885df4ae9 | ||
|
|
89bc6da5f4 | ||
|
|
d87f698512 | ||
|
|
08dd871cb8 | ||
|
|
b5578eadf7 | ||
|
|
aadc1bb84c | ||
|
|
2823af9441 | ||
|
|
d7da83359f | ||
|
|
a143cd3427 | ||
|
|
3c4dcde1d4 | ||
|
|
7adfc195dc | ||
|
|
5a2c315b20 | ||
|
|
299803f03c | ||
|
|
1eac62a26e | ||
|
|
f1b5416d0b | ||
|
|
65168c8532 | ||
|
|
35f6476d0f | ||
|
|
36fabe194f | ||
|
|
921c700fab | ||
|
|
2dbe35a31c | ||
|
|
656d13d79b | ||
|
|
77b1adae37 | ||
|
|
c18373bb0e | ||
|
|
d4e7563272 | ||
|
|
86d6052c89 | ||
|
|
c5ae0dc4ca | ||
|
|
e979a2be9b | ||
|
|
586b84f434 | ||
|
|
56b9d22d49 | ||
|
|
69aa241dc9 | ||
|
|
1335e12b97 | ||
|
|
d1b1fa7ffd | ||
|
|
d6a3492e90 | ||
|
|
4af57810d6 | ||
|
|
6555cc4639 | ||
|
|
3f57287d79 | ||
|
|
1713e311f3 | ||
|
|
8a14e78d2d | ||
|
|
1942e55f76 | ||
|
|
d83f41d0ff | ||
|
|
0f09240fb2 | ||
|
|
3731b49ea8 | ||
|
|
209223f77e | ||
|
|
7a5f5ee31d | ||
|
|
a148cb6c9b | ||
|
|
e9ac049744 | ||
|
|
e06d4bd841 | ||
|
|
6cad4f5839 | ||
|
|
86f5f9eba3 | ||
|
|
473d091fa8 | ||
|
|
aec5ad4099 | ||
|
|
f14f98f7c1 | ||
|
|
e3d9a7ddf2 | ||
|
|
58e4f9b7b4 | ||
|
|
ef1f09cd4a | ||
|
|
617619eb31 | ||
|
|
00a135b00f | ||
|
|
c71104db4f | ||
|
|
eef7940fbc | ||
|
|
da4b3db17a | ||
|
|
c0d20f04b6 | ||
|
|
8fda8668b7 | ||
|
|
ea2c641604 | ||
|
|
84e38505c5 | ||
|
|
6584eb0827 | ||
|
|
467d897e05 | ||
|
|
2e6ea202cd | ||
|
|
27f17551ad | ||
|
|
48bfc4e3cd | ||
|
|
61b9a4cf28 | ||
|
|
c95448bf25 | ||
|
|
f1ca60c182 | ||
|
|
52e79f78e5 | ||
|
|
586d6876f1 | ||
|
|
25759ecf0a | ||
|
|
1fbe870884 | ||
|
|
9fcd497c42 | ||
|
|
63ee6b7f0e | ||
|
|
73c0cd6934 | ||
|
|
e6c95a0913 | ||
|
|
af11cae29c | ||
|
|
b984a9ff00 | ||
|
|
13837e0bf3 | ||
|
|
f5d19fd28a | ||
|
|
24ac3ea37d | ||
|
|
13cb33cc4a | ||
|
|
949a4697fe | ||
|
|
3bbb828284 | ||
|
|
942b0f3dc9 | ||
|
|
208d8845c4 | ||
|
|
24cac9dcd5 | ||
|
|
c8b29da677 | ||
|
|
4f63d14529 | ||
|
|
cef6248650 | ||
|
|
774e443a79 | ||
|
|
1166807434 | ||
|
|
99cd502b65 | ||
|
|
d959e72a9c | ||
|
|
ee83788b43 | ||
|
|
62dd5f8ed7 | ||
|
|
2de9984945 | ||
|
|
890b46b697 | ||
|
|
5419957e06 | ||
|
|
39d4667916 | ||
|
|
083db67df9 | ||
|
|
8dceb6032b | ||
|
|
c300dad316 | ||
|
|
45b07f46f1 | ||
|
|
4023127c87 | ||
|
|
ab96c549ae | ||
|
|
bc0afb589e | ||
|
|
b42127f083 | ||
|
|
61d5a964ee | ||
|
|
f8fd6b78f5 | ||
|
|
4546ef6619 | ||
|
|
1f4457d9ca | ||
|
|
65cbbf78bc | ||
|
|
a73a24c1df | ||
|
|
31f850c19c | ||
|
|
6d90d7bc82 | ||
|
|
d2a1c02b92 | ||
|
|
6d96452ef8 | ||
|
|
833589e6e7 | ||
|
|
8bb566e189 | ||
|
|
38d2117752 | ||
|
|
914decd4f9 | ||
|
|
873c38f9e1 | ||
|
|
a3e37eca62 | ||
|
|
817911a41e | ||
|
|
9f4fce9daa | ||
|
|
9ff305d5db | ||
|
|
055c3e098f | ||
|
|
bc61dd85c6 | ||
|
|
db6f1405e9 | ||
|
|
3dc3376aec | ||
|
|
55920a58a3 | ||
|
|
2a70ebf667 | ||
|
|
2f65a86aa0 | ||
|
|
4bf81ac33b | ||
|
|
263c23ae8f | ||
|
|
bf51b945c5 | ||
|
|
9d7a461550 | ||
|
|
bbf60818eb | ||
|
|
103b22ebb2 | ||
|
|
cf4a1d7d40 | ||
|
|
e94f036aca |
1
.commitlintrc.json
Normal file
1
.commitlintrc.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": ["@commitlint/config-conventional"] }
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.{spec,test}.{,c,m}js'],
|
||||
files: ['*.{integ,spec,test}.{,c,m}js'],
|
||||
rules: {
|
||||
'n/no-unpublished-require': 'off',
|
||||
'n/no-unpublished-import': 'off',
|
||||
|
||||
32
.github/workflows/ci.yml
vendored
Normal file
32
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Continous Integration
|
||||
on: push
|
||||
jobs:
|
||||
CI:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install packages
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl qemu-utils python3-vmdkstream git libxml2-utils libfuse2 nbdkit
|
||||
- name: Cache Turbo
|
||||
# https://github.com/actions/cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules/.cache/turbo'
|
||||
key: ${{ runner.os }}-turbo-cache
|
||||
- name: Setup Node environment
|
||||
# https://github.com/actions/setup-node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
- name: Install project dependencies
|
||||
run: yarn
|
||||
- name: Build the project
|
||||
run: yarn build
|
||||
- name: Lint tests
|
||||
run: yarn test-lint
|
||||
- name: Integration tests
|
||||
run: sudo yarn test-integration
|
||||
12
.github/workflows/push.yml
vendored
12
.github/workflows/push.yml
vendored
@@ -1,12 +0,0 @@
|
||||
name: CI
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Build docker image
|
||||
run: docker-compose -f docker/docker-compose.dev.yml build
|
||||
- name: Create the container and start the tests
|
||||
run: docker-compose -f docker/docker-compose.dev.yml up --exit-code-from xo
|
||||
11
.husky/commit-msg
Executable file
11
.husky/commit-msg
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Only check commit message if commit on master or first commit on another
|
||||
# branch to avoid bothering fix commits after reviews
|
||||
#
|
||||
# FIXME: does not properly run with git commit --amend
|
||||
if [ "$(git rev-parse --abbrev-ref HEAD)" = master ] || [ "$(git rev-list --count master..)" -eq 0 ]
|
||||
then
|
||||
npx --no -- commitlint --edit "$1"
|
||||
fi
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/async-each):
|
||||
|
||||
```
|
||||
> npm install --save @vates/async-each
|
||||
```sh
|
||||
npm install --save @vates/async-each
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/cached-dns.lookup):
|
||||
|
||||
```
|
||||
> npm install --save @vates/cached-dns.lookup
|
||||
```sh
|
||||
npm install --save @vates/cached-dns.lookup
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
|
||||
|
||||
```
|
||||
> npm install --save @vates/coalesce-calls
|
||||
```sh
|
||||
npm install --save @vates/coalesce-calls
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/compose):
|
||||
|
||||
```
|
||||
> npm install --save @vates/compose
|
||||
```sh
|
||||
npm install --save @vates/compose
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with):
|
||||
|
||||
```
|
||||
> npm install --save @vates/decorate-with
|
||||
```sh
|
||||
npm install --save @vates/decorate-with
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
32
@vates/diff/.USAGE.md
Normal file
32
@vates/diff/.USAGE.md
Normal file
@@ -0,0 +1,32 @@
|
||||
```js
|
||||
import diff from '@vates/diff'
|
||||
|
||||
diff('foo bar baz', 'Foo qux')
|
||||
// → [ 0, 'F', 4, 'qux', 7, '' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains `F`
|
||||
// - at position 4, it contains `qux`
|
||||
// - at position 7, it ends
|
||||
|
||||
diff('Foo qux', 'foo bar baz')
|
||||
// → [ 0, 'f', 4, 'bar', 7, ' baz' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains f`
|
||||
// - at position 4, it contains `bar`
|
||||
// - at position 7, it contains `baz`
|
||||
|
||||
// works with all collections that supports
|
||||
// - `.length`
|
||||
// - `collection[index]`
|
||||
// - `.slice(start, end)`
|
||||
//
|
||||
// which includes:
|
||||
// - arrays
|
||||
// - strings
|
||||
// - `Buffer`
|
||||
// - `TypedArray`
|
||||
diff([0, 1, 2], [3, 4])
|
||||
// → [ 0, [ 3, 4 ], 2, [] ]
|
||||
```
|
||||
1
@vates/diff/.npmignore
Symbolic link
1
@vates/diff/.npmignore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/npmignore
|
||||
65
@vates/diff/README.md
Normal file
65
@vates/diff/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/diff
|
||||
|
||||
[](https://npmjs.org/package/@vates/diff)  [](https://bundlephobia.com/result?p=@vates/diff) [](https://npmjs.org/package/@vates/diff)
|
||||
|
||||
> Computes differences between two arrays, buffers or strings
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/diff):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/diff
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import diff from '@vates/diff'
|
||||
|
||||
diff('foo bar baz', 'Foo qux')
|
||||
// → [ 0, 'F', 4, 'qux', 7, '' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains `F`
|
||||
// - at position 4, it contains `qux`
|
||||
// - at position 7, it ends
|
||||
|
||||
diff('Foo qux', 'foo bar baz')
|
||||
// → [ 0, 'f', 4, 'bar', 7, ' baz' ]
|
||||
//
|
||||
// Differences of the second string from the first one:
|
||||
// - at position 0, it contains f`
|
||||
// - at position 4, it contains `bar`
|
||||
// - at position 7, it contains `baz`
|
||||
|
||||
// works with all collections that supports
|
||||
// - `.length`
|
||||
// - `collection[index]`
|
||||
// - `.slice(start, end)`
|
||||
//
|
||||
// which includes:
|
||||
// - arrays
|
||||
// - strings
|
||||
// - `Buffer`
|
||||
// - `TypedArray`
|
||||
diff([0, 1, 2], [3, 4])
|
||||
// → [ 0, [ 3, 4 ], 2, [] ]
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
37
@vates/diff/index.js
Normal file
37
@vates/diff/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Compare two data arrays, buffers or strings and invoke the provided callback function for each difference.
|
||||
*
|
||||
* @template {Array|Buffer|string} T
|
||||
* @param {Array|Buffer|string} data1 - The first data array or buffer to compare.
|
||||
* @param {T} data2 - The second data array or buffer to compare.
|
||||
* @param {(index: number, diff: T) => void} [cb] - The callback function to invoke for each difference. If not provided, an array of differences will be returned.
|
||||
* @returns {Array<number|T>|undefined} - An array of differences if no callback is provided, otherwise undefined.
|
||||
*/
|
||||
module.exports = function diff(data1, data2, cb) {
|
||||
let result
|
||||
if (cb === undefined) {
|
||||
result = []
|
||||
cb = result.push.bind(result)
|
||||
}
|
||||
|
||||
const n1 = data1.length
|
||||
const n2 = data2.length
|
||||
const n = Math.min(n1, n2)
|
||||
for (let i = 0; i < n; ++i) {
|
||||
if (data1[i] !== data2[i]) {
|
||||
let j = i + 1
|
||||
while (j < n && data1[j] !== data2[j]) {
|
||||
++j
|
||||
}
|
||||
cb(i, data2.slice(i, j))
|
||||
i = j
|
||||
}
|
||||
}
|
||||
if (n1 !== n2) {
|
||||
cb(n, n1 < n2 ? data2.slice(n) : data2.slice(0, 0))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
51
@vates/diff/index.test.js
Normal file
51
@vates/diff/index.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert/strict')
|
||||
const test = require('test')
|
||||
|
||||
const diff = require('./index.js')
|
||||
|
||||
test('data of equal length', function () {
|
||||
const data1 = 'foo bar baz'
|
||||
const data2 = 'baz bar foo'
|
||||
assert.deepEqual(diff(data1, data2), [0, 'baz', 8, 'foo'])
|
||||
})
|
||||
|
||||
test('data1 is longer', function () {
|
||||
const data1 = 'foo bar'
|
||||
const data2 = 'foo'
|
||||
assert.deepEqual(diff(data1, data2), [3, ''])
|
||||
})
|
||||
|
||||
test('data2 is longer', function () {
|
||||
const data1 = 'foo'
|
||||
const data2 = 'foo bar'
|
||||
assert.deepEqual(diff(data1, data2), [3, ' bar'])
|
||||
})
|
||||
|
||||
test('with arrays', function () {
|
||||
const data1 = 'foo bar baz'.split('')
|
||||
const data2 = 'baz bar foo'.split('')
|
||||
assert.deepEqual(diff(data1, data2), [0, 'baz'.split(''), 8, 'foo'.split('')])
|
||||
})
|
||||
|
||||
test('with buffers', function () {
|
||||
const data1 = Buffer.from('foo bar baz')
|
||||
const data2 = Buffer.from('baz bar foo')
|
||||
assert.deepEqual(diff(data1, data2), [0, Buffer.from('baz'), 8, Buffer.from('foo')])
|
||||
})
|
||||
|
||||
test('cb param', function () {
|
||||
const data1 = 'foo bar baz'
|
||||
const data2 = 'baz bar foo'
|
||||
|
||||
const calls = []
|
||||
const cb = (...args) => calls.push(args)
|
||||
|
||||
diff(data1, data2, cb)
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
[0, 'baz'],
|
||||
[8, 'foo'],
|
||||
])
|
||||
})
|
||||
36
@vates/diff/package.json
Normal file
36
@vates/diff/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/diff",
|
||||
"description": "Computes differences between two arrays, buffers or strings",
|
||||
"keywords": [
|
||||
"array",
|
||||
"binary",
|
||||
"buffer",
|
||||
"diff",
|
||||
"differences",
|
||||
"string"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/diff",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/diff",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.3.0"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/disposable):
|
||||
|
||||
```
|
||||
> npm install --save @vates/disposable
|
||||
```sh
|
||||
npm install --save @vates/disposable
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/event-listeners-manager):
|
||||
|
||||
```
|
||||
> npm install --save @vates/event-listeners-manager
|
||||
```sh
|
||||
npm install --save @vates/event-listeners-manager
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.2.1"
|
||||
"vhd-lib": "^4.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/multi-key-map):
|
||||
|
||||
```
|
||||
> npm install --save @vates/multi-key-map
|
||||
```sh
|
||||
npm install --save @vates/multi-key-map
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/nbd-client):
|
||||
|
||||
```
|
||||
> npm install --save @vates/nbd-client
|
||||
```sh
|
||||
npm install --save @vates/nbd-client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -16,9 +16,13 @@ const {
|
||||
NBD_REPLY_MAGIC,
|
||||
NBD_REQUEST_MAGIC,
|
||||
OPTS_MAGIC,
|
||||
NBD_CMD_DISC,
|
||||
} = require('./constants.js')
|
||||
const { fromCallback } = require('promise-toolbox')
|
||||
const { fromCallback, pRetry, pDelay, pTimeout } = require('promise-toolbox')
|
||||
const { readChunkStrict } = require('@vates/read-chunk')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
|
||||
const { warn } = createLogger('vates:nbd-client')
|
||||
|
||||
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
|
||||
|
||||
@@ -31,18 +35,34 @@ module.exports = class NbdClient {
|
||||
#exportName
|
||||
#exportSize
|
||||
|
||||
#waitBeforeReconnect
|
||||
#readAhead
|
||||
#readBlockRetries
|
||||
#reconnectRetry
|
||||
#connectTimeout
|
||||
|
||||
// AFAIK, there is no guaranty the server answers in the same order as the queries
|
||||
// so we handle a backlog of command waiting for response and handle concurrency manually
|
||||
|
||||
#waitingForResponse // there is already a listenner waiting for a response
|
||||
#nextCommandQueryId = BigInt(0)
|
||||
#commandQueryBacklog // map of command waiting for an response queryId => { size/*in byte*/, resolve, reject}
|
||||
#connected = false
|
||||
|
||||
constructor({ address, port = NBD_DEFAULT_PORT, exportname, cert }) {
|
||||
#reconnectingPromise
|
||||
constructor(
|
||||
{ address, port = NBD_DEFAULT_PORT, exportname, cert },
|
||||
{ connectTimeout = 6e4, waitBeforeReconnect = 1e3, readAhead = 10, readBlockRetries = 5, reconnectRetry = 5 } = {}
|
||||
) {
|
||||
this.#serverAddress = address
|
||||
this.#serverPort = port
|
||||
this.#exportName = exportname
|
||||
this.#serverCert = cert
|
||||
this.#waitBeforeReconnect = waitBeforeReconnect
|
||||
this.#readAhead = readAhead
|
||||
this.#readBlockRetries = readBlockRetries
|
||||
this.#reconnectRetry = reconnectRetry
|
||||
this.#connectTimeout = connectTimeout
|
||||
}
|
||||
|
||||
get exportSize() {
|
||||
@@ -77,19 +97,55 @@ module.exports = class NbdClient {
|
||||
})
|
||||
}
|
||||
|
||||
async connect() {
|
||||
// first we connect to the serve without tls, and then we upgrade the connection
|
||||
async #connect() {
|
||||
// first we connect to the server without tls, and then we upgrade the connection
|
||||
// to tls during the handshake
|
||||
await this.#unsecureConnect()
|
||||
await this.#handshake()
|
||||
|
||||
this.#connected = true
|
||||
// reset internal state if we reconnected a nbd client
|
||||
this.#commandQueryBacklog = new Map()
|
||||
this.#waitingForResponse = false
|
||||
}
|
||||
async connect() {
|
||||
return pTimeout.call(this.#connect(), this.#connectTimeout)
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (!this.#connected) {
|
||||
return
|
||||
}
|
||||
|
||||
const buffer = Buffer.alloc(28)
|
||||
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
|
||||
buffer.writeInt16BE(0, 4) // no command flags for a disconnect
|
||||
buffer.writeInt16BE(NBD_CMD_DISC, 6) // we want to disconnect from nbd server
|
||||
await this.#write(buffer)
|
||||
await this.#serverSocket.destroy()
|
||||
this.#serverSocket = undefined
|
||||
this.#connected = false
|
||||
}
|
||||
|
||||
#clearReconnectPromise = () => {
|
||||
this.#reconnectingPromise = undefined
|
||||
}
|
||||
|
||||
async #reconnect() {
|
||||
await this.disconnect().catch(() => {})
|
||||
await pDelay(this.#waitBeforeReconnect) // need to let the xapi clean things on its side
|
||||
await this.connect()
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
// we need to ensure reconnections do not occur in parallel
|
||||
if (this.#reconnectingPromise === undefined) {
|
||||
this.#reconnectingPromise = pRetry(() => this.#reconnect(), {
|
||||
tries: this.#reconnectRetry,
|
||||
})
|
||||
this.#reconnectingPromise.then(this.#clearReconnectPromise, this.#clearReconnectPromise)
|
||||
}
|
||||
|
||||
return this.#reconnectingPromise
|
||||
}
|
||||
|
||||
// we can use individual read/write from the socket here since there is no concurrency
|
||||
@@ -167,7 +223,6 @@ module.exports = class NbdClient {
|
||||
this.#commandQueryBacklog.forEach(({ reject }) => {
|
||||
reject(error)
|
||||
})
|
||||
await this.disconnect()
|
||||
}
|
||||
|
||||
async #readBlockResponse() {
|
||||
@@ -175,7 +230,6 @@ module.exports = class NbdClient {
|
||||
if (this.#waitingForResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.#waitingForResponse = true
|
||||
const magic = await this.#readInt32()
|
||||
@@ -200,7 +254,8 @@ module.exports = class NbdClient {
|
||||
query.resolve(data)
|
||||
this.#waitingForResponse = false
|
||||
if (this.#commandQueryBacklog.size > 0) {
|
||||
await this.#readBlockResponse()
|
||||
// it doesn't throw directly but will throw all relevant promise on failure
|
||||
this.#readBlockResponse()
|
||||
}
|
||||
} catch (error) {
|
||||
// reject all the promises
|
||||
@@ -211,6 +266,11 @@ module.exports = class NbdClient {
|
||||
}
|
||||
|
||||
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
|
||||
// we don't want to add anything in backlog while reconnecting
|
||||
if (this.#reconnectingPromise) {
|
||||
await this.#reconnectingPromise
|
||||
}
|
||||
|
||||
const queryId = this.#nextCommandQueryId
|
||||
this.#nextCommandQueryId++
|
||||
|
||||
@@ -225,19 +285,67 @@ module.exports = class NbdClient {
|
||||
buffer.writeInt32BE(size, 24)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
function decoratedReject(error) {
|
||||
error.index = index
|
||||
error.size = size
|
||||
reject(error)
|
||||
}
|
||||
|
||||
// this will handle one block response, but it can be another block
|
||||
// since server does not guaranty to handle query in order
|
||||
this.#commandQueryBacklog.set(queryId, {
|
||||
size,
|
||||
resolve,
|
||||
reject,
|
||||
reject: decoratedReject,
|
||||
})
|
||||
// really send the command to the server
|
||||
this.#write(buffer).catch(reject)
|
||||
this.#write(buffer).catch(decoratedReject)
|
||||
|
||||
// #readBlockResponse never throws directly
|
||||
// but if it fails it will reject all the promises in the backlog
|
||||
this.#readBlockResponse()
|
||||
})
|
||||
}
|
||||
|
||||
async *readBlocks(indexGenerator) {
|
||||
// default : read all blocks
|
||||
if (indexGenerator === undefined) {
|
||||
const exportSize = this.#exportSize
|
||||
const chunkSize = 2 * 1024 * 1024
|
||||
indexGenerator = function* () {
|
||||
const nbBlocks = Math.ceil(exportSize / chunkSize)
|
||||
for (let index = 0; index < nbBlocks; index++) {
|
||||
yield { index, size: chunkSize }
|
||||
}
|
||||
}
|
||||
}
|
||||
const readAhead = []
|
||||
const readAheadMaxLength = this.#readAhead
|
||||
const makeReadBlockPromise = (index, size) => {
|
||||
const promise = pRetry(() => this.readBlock(index, size), {
|
||||
tries: this.#readBlockRetries,
|
||||
onRetry: async err => {
|
||||
warn('will retry reading block ', index, err)
|
||||
await this.reconnect()
|
||||
},
|
||||
})
|
||||
// error is handled during unshift
|
||||
promise.catch(() => {})
|
||||
return promise
|
||||
}
|
||||
|
||||
// read all blocks, but try to keep readAheadMaxLength promise waiting ahead
|
||||
for (const { index, size } of indexGenerator()) {
|
||||
// stack readAheadMaxLength promises before starting to handle the results
|
||||
if (readAhead.length === readAheadMaxLength) {
|
||||
// any error will stop reading blocks
|
||||
yield readAhead.shift()
|
||||
}
|
||||
|
||||
readAhead.push(makeReadBlockPromise(index, size))
|
||||
}
|
||||
while (readAhead.length > 0) {
|
||||
yield readAhead.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,17 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@vates/read-chunk": "^1.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^1.2.2"
|
||||
"xen-api": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0",
|
||||
@@ -30,6 +31,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test-integration": "tap *.spec.js"
|
||||
"test-integration": "tap --lines 70 --functions 36 --branches 54 --statements 69 *.integ.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/otp):
|
||||
|
||||
```
|
||||
> npm install --save @vates/otp
|
||||
```sh
|
||||
npm install --save @vates/otp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/parse-duration):
|
||||
|
||||
```
|
||||
> npm install --save @vates/parse-duration
|
||||
```sh
|
||||
npm install --save @vates/parse-duration
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/predicates):
|
||||
|
||||
```
|
||||
> npm install --save @vates/predicates
|
||||
```sh
|
||||
npm install --save @vates/predicates
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -24,3 +24,25 @@ import { readChunkStrict } from '@vates/read-chunk'
|
||||
|
||||
const chunk = await readChunkStrict(stream, 1024)
|
||||
```
|
||||
|
||||
### `skip(stream, size)`
|
||||
|
||||
Skips a given number of bytes from a stream.
|
||||
|
||||
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
|
||||
|
||||
```js
|
||||
import { skip } from '@vates/read-chunk'
|
||||
|
||||
const bytesSkipped = await skip(stream, 2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
|
||||
### `skipStrict(stream, size)`
|
||||
|
||||
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
|
||||
|
||||
```js
|
||||
import { skipStrict } from '@vates/read-chunk'
|
||||
|
||||
await skipStrict(stream, 2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/read-chunk):
|
||||
|
||||
```
|
||||
> npm install --save @vates/read-chunk
|
||||
```sh
|
||||
npm install --save @vates/read-chunk
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -43,6 +43,28 @@ import { readChunkStrict } from '@vates/read-chunk'
|
||||
const chunk = await readChunkStrict(stream, 1024)
|
||||
```
|
||||
|
||||
### `skip(stream, size)`
|
||||
|
||||
Skips a given number of bytes from a stream.
|
||||
|
||||
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
|
||||
|
||||
```js
|
||||
import { skip } from '@vates/read-chunk'
|
||||
|
||||
const bytesSkipped = await skip(stream, 2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
|
||||
### `skipStrict(stream, size)`
|
||||
|
||||
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
|
||||
|
||||
```js
|
||||
import { skipStrict } from '@vates/read-chunk'
|
||||
|
||||
await skipStrict(stream, 2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
|
||||
/**
|
||||
* Read a chunk of data from a stream.
|
||||
*
|
||||
* The returned promise is rejected if there is an error while reading the stream.
|
||||
*
|
||||
* For streams in object mode, the returned promise resolves to a single object read from the stream.
|
||||
*
|
||||
* For streams in binary mode, the returned promise resolves to a Buffer or a string if an encoding has been specified using the `stream.setEncoding()` method.
|
||||
*
|
||||
* If `size` bytes are not available to be read, `null` will be returned *unless* the stream has ended, in which case all of the data remaining will be returned.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to read from.
|
||||
* @param {number} [size] - The number of bytes to read for binary streams (ignored for object streams).
|
||||
* @returns {Promise<Buffer|string|unknown|null>} - A Promise that resolves to the read chunk if available, or null if end of stream is reached.
|
||||
*/
|
||||
const readChunk = (stream, size) =>
|
||||
stream.closed || stream.readableEnded
|
||||
stream.errored != null
|
||||
? Promise.reject(stream.errored)
|
||||
: stream.closed || stream.readableEnded
|
||||
? Promise.resolve(null)
|
||||
: size === 0
|
||||
? Promise.resolve(Buffer.alloc(0))
|
||||
: new Promise((resolve, reject) => {
|
||||
if (size !== undefined) {
|
||||
assert(size > 0)
|
||||
|
||||
// per Node documentation:
|
||||
// > The size argument must be less than or equal to 1 GiB.
|
||||
assert(size < 1073741824)
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
resolve(null)
|
||||
removeListeners()
|
||||
@@ -33,6 +58,21 @@ const readChunk = (stream, size) =>
|
||||
})
|
||||
exports.readChunk = readChunk
|
||||
|
||||
/**
|
||||
* Read a chunk of data from a stream.
|
||||
*
|
||||
* The returned promise is rejected if there is an error while reading the stream.
|
||||
*
|
||||
* For streams in object mode, the returned promise resolves to a single object read from the stream.
|
||||
*
|
||||
* For streams in binary mode, the returned promise resolves to a Buffer or a string if an encoding has been specified using the `stream.setEncoding()` method.
|
||||
*
|
||||
* If `size` bytes are not available to be read, the returned promise is rejected.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to read from.
|
||||
* @param {number} [size] - The number of bytes to read for binary streams (ignored for object streams).
|
||||
* @returns {Promise<Buffer|string|unknown>} - A Promise that resolves to the read chunk.
|
||||
*/
|
||||
exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
const chunk = await readChunk(stream, size)
|
||||
if (chunk === null) {
|
||||
@@ -40,7 +80,7 @@ exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
}
|
||||
|
||||
if (size !== undefined && chunk.length !== size) {
|
||||
const error = new Error('stream has ended with not enough data')
|
||||
const error = new Error(`stream has ended with not enough data (actual: ${chunk.length}, expected: ${size})`)
|
||||
Object.defineProperties(error, {
|
||||
chunk: {
|
||||
value: chunk,
|
||||
@@ -51,3 +91,69 @@ exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
|
||||
return chunk
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips a given number of bytes from a readable stream.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to skip bytes from.
|
||||
* @param {number} size - The number of bytes to skip.
|
||||
* @returns {Promise<number>} A Promise that resolves to the number of bytes actually skipped. If the end of the stream is reached before all bytes are skipped, the Promise resolves to the number of bytes that were skipped before the end of the stream was reached. The Promise is rejected if there is an error while reading from the stream.
|
||||
*/
|
||||
async function skip(stream, size) {
|
||||
return stream.errored != null
|
||||
? Promise.reject(stream.errored)
|
||||
: size === 0 || stream.closed || stream.readableEnded
|
||||
? Promise.resolve(0)
|
||||
: new Promise((resolve, reject) => {
|
||||
let left = size
|
||||
function onEnd() {
|
||||
resolve(size - left)
|
||||
removeListeners()
|
||||
}
|
||||
function onError(error) {
|
||||
reject(error)
|
||||
removeListeners()
|
||||
}
|
||||
function onReadable() {
|
||||
const data = stream.read()
|
||||
left -= data === null ? 0 : data.length
|
||||
if (left > 0) {
|
||||
// continue to read
|
||||
} else {
|
||||
// if more than wanted has been read, push back the rest
|
||||
if (left < 0) {
|
||||
stream.unshift(data.slice(left))
|
||||
}
|
||||
|
||||
resolve(size)
|
||||
removeListeners()
|
||||
}
|
||||
}
|
||||
function removeListeners() {
|
||||
stream.removeListener('end', onEnd)
|
||||
stream.removeListener('error', onError)
|
||||
stream.removeListener('readable', onReadable)
|
||||
}
|
||||
stream.on('end', onEnd)
|
||||
stream.on('error', onError)
|
||||
stream.on('readable', onReadable)
|
||||
onReadable()
|
||||
})
|
||||
}
|
||||
exports.skip = skip
|
||||
|
||||
/**
|
||||
* Skips a given number of bytes from a stream.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to skip bytes from.
|
||||
* @param {number} size - The number of bytes to skip.
|
||||
* @returns {Promise<void>} - A Promise that resolves when the exact number of bytes have been skipped. The Promise is rejected if there is an error while reading from the stream or the stream ends before the exact number of bytes have been skipped.
|
||||
*/
|
||||
exports.skipStrict = async function skipStrict(stream, size) {
|
||||
const bytesSkipped = await skip(stream, size)
|
||||
if (bytesSkipped !== size) {
|
||||
const error = new Error(`stream has ended with not enough data (actual: ${bytesSkipped}, expected: ${size})`)
|
||||
error.bytesSkipped = bytesSkipped
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,58 @@ const assert = require('node:assert').strict
|
||||
|
||||
const { Readable } = require('stream')
|
||||
|
||||
const { readChunk, readChunkStrict } = require('./')
|
||||
const { readChunk, readChunkStrict, skip, skipStrict } = require('./')
|
||||
|
||||
const makeStream = it => Readable.from(it, { objectMode: false })
|
||||
makeStream.obj = Readable.from
|
||||
|
||||
const rejectionOf = promise =>
|
||||
promise.then(
|
||||
value => {
|
||||
throw value
|
||||
},
|
||||
error => error
|
||||
)
|
||||
|
||||
const makeErrorTests = fn => {
|
||||
it('rejects if the stream errors', async () => {
|
||||
const error = new Error()
|
||||
const stream = makeStream([])
|
||||
|
||||
const pError = rejectionOf(fn(stream, 10))
|
||||
stream.destroy(error)
|
||||
|
||||
assert.strict(await pError, error)
|
||||
})
|
||||
|
||||
// only supported for Node >= 18
|
||||
if (process.versions.node.split('.')[0] >= 18) {
|
||||
it('rejects if the stream has already errored', async () => {
|
||||
const error = new Error()
|
||||
const stream = makeStream([])
|
||||
|
||||
await new Promise(resolve => {
|
||||
stream.once('error', resolve).destroy(error)
|
||||
})
|
||||
|
||||
assert.strict(await rejectionOf(fn(stream, 10)), error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('readChunk', () => {
|
||||
it('rejects if size is less than or equal to 0', async () => {
|
||||
const error = await rejectionOf(readChunk(makeStream([]), 0))
|
||||
assert.strictEqual(error.code, 'ERR_ASSERTION')
|
||||
})
|
||||
|
||||
it('rejects if size is greater than or equal to 1 GiB', async () => {
|
||||
const error = await rejectionOf(readChunk(makeStream([]), 1024 * 1024 * 1024))
|
||||
assert.strictEqual(error.code, 'ERR_ASSERTION')
|
||||
})
|
||||
|
||||
makeErrorTests(readChunk)
|
||||
|
||||
it('returns null if stream is empty', async () => {
|
||||
assert.strictEqual(await readChunk(makeStream([])), null)
|
||||
})
|
||||
@@ -38,10 +84,6 @@ describe('readChunk', () => {
|
||||
it('returns less data if stream ends', async () => {
|
||||
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 10), Buffer.from('foobar'))
|
||||
})
|
||||
|
||||
it('returns an empty buffer if the specified size is 0', async () => {
|
||||
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 0), Buffer.alloc(0))
|
||||
})
|
||||
})
|
||||
|
||||
describe('with object stream', () => {
|
||||
@@ -52,14 +94,6 @@ describe('readChunk', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const rejectionOf = promise =>
|
||||
promise.then(
|
||||
value => {
|
||||
throw value
|
||||
},
|
||||
error => error
|
||||
)
|
||||
|
||||
describe('readChunkStrict', function () {
|
||||
it('throws if stream is empty', async () => {
|
||||
const error = await rejectionOf(readChunkStrict(makeStream([])))
|
||||
@@ -71,7 +105,43 @@ describe('readChunkStrict', function () {
|
||||
it('throws if stream ends with not enough data', async () => {
|
||||
const error = await rejectionOf(readChunkStrict(makeStream(['foo', 'bar']), 10))
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data')
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 6, expected: 10)')
|
||||
assert.deepEqual(error.chunk, Buffer.from('foobar'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('skip', function () {
|
||||
makeErrorTests(skip)
|
||||
|
||||
it('returns 0 if size is 0', async () => {
|
||||
assert.strictEqual(await skip(makeStream(['foo']), 0), 0)
|
||||
})
|
||||
|
||||
it('returns 0 if the stream is already ended', async () => {
|
||||
const stream = await makeStream([])
|
||||
await readChunk(stream)
|
||||
|
||||
assert.strictEqual(await skip(stream, 10), 0)
|
||||
})
|
||||
|
||||
it('skips a number of bytes', async () => {
|
||||
const stream = makeStream('foo bar')
|
||||
|
||||
assert.strictEqual(await skip(stream, 4), 4)
|
||||
assert.deepEqual(await readChunk(stream, 4), Buffer.from('bar'))
|
||||
})
|
||||
|
||||
it('returns less size if stream ends', async () => {
|
||||
assert.deepEqual(await skip(makeStream('foo bar'), 10), 7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('skipStrict', function () {
|
||||
it('throws if stream ends with not enough data', async () => {
|
||||
const error = await rejectionOf(skipStrict(makeStream('foo bar'), 10))
|
||||
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 7, expected: 10)')
|
||||
assert.deepEqual(error.bytesSkipped, 7)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.1",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
|
||||
42
@vates/stream-reader/.USAGE.md
Normal file
42
@vates/stream-reader/.USAGE.md
Normal file
@@ -0,0 +1,42 @@
|
||||
```js
|
||||
import StreamReader from '@vates/stream-reader'
|
||||
|
||||
const reader = new StreamReader(stream)
|
||||
```
|
||||
|
||||
### `.read([size])`
|
||||
|
||||
- returns the next available chunk of data
|
||||
- like `stream.read()`, a number of bytes can be specified
|
||||
- returns with less data than expected if stream has ended
|
||||
- returns `null` if the stream has ended and no data has been read
|
||||
|
||||
```js
|
||||
const chunk = await reader.read(512)
|
||||
```
|
||||
|
||||
### `.readStrict([size])`
|
||||
|
||||
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
|
||||
|
||||
```js
|
||||
const chunk = await reader.readStrict(512)
|
||||
```
|
||||
|
||||
### `.skip(size)`
|
||||
|
||||
Skips a given number of bytes from a stream.
|
||||
|
||||
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
|
||||
|
||||
```js
|
||||
const bytesSkipped = await reader.skip(2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
|
||||
### `.skipStrict(size)`
|
||||
|
||||
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
|
||||
|
||||
```js
|
||||
await reader.skipStrict(2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
1
@vates/stream-reader/.npmignore
Symbolic link
1
@vates/stream-reader/.npmignore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/npmignore
|
||||
75
@vates/stream-reader/README.md
Normal file
75
@vates/stream-reader/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/stream-reader
|
||||
|
||||
[](https://npmjs.org/package/@vates/stream-reader)  [](https://bundlephobia.com/result?p=@vates/stream-reader) [](https://npmjs.org/package/@vates/stream-reader)
|
||||
|
||||
> Efficiently reads and skips chunks of a given size in a stream
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/stream-reader):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/stream-reader
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import StreamReader from '@vates/stream-reader'
|
||||
|
||||
const reader = new StreamReader(stream)
|
||||
```
|
||||
|
||||
### `.read([size])`
|
||||
|
||||
- returns the next available chunk of data
|
||||
- like `stream.read()`, a number of bytes can be specified
|
||||
- returns with less data than expected if stream has ended
|
||||
- returns `null` if the stream has ended and no data has been read
|
||||
|
||||
```js
|
||||
const chunk = await reader.read(512)
|
||||
```
|
||||
|
||||
### `.readStrict([size])`
|
||||
|
||||
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
|
||||
|
||||
```js
|
||||
const chunk = await reader.readStrict(512)
|
||||
```
|
||||
|
||||
### `.skip(size)`
|
||||
|
||||
Skips a given number of bytes from a stream.
|
||||
|
||||
Returns the number of bytes actually skipped, which may be less than the requested size if the stream has ended.
|
||||
|
||||
```js
|
||||
const bytesSkipped = await reader.skip(2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
|
||||
### `.skipStrict(size)`
|
||||
|
||||
Skips a given number of bytes from a stream and throws if the stream ended before enough stream has been skipped.
|
||||
|
||||
```js
|
||||
await reader.skipStrict(2 * 1024 * 1024 * 1024)
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
123
@vates/stream-reader/index.js
Normal file
123
@vates/stream-reader/index.js
Normal file
@@ -0,0 +1,123 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert')
|
||||
const { finished, Readable } = require('node:stream')
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
// Inspired by https://github.com/nodejs/node/blob/85705a47958c9ae5dbaa1f57456db19bdefdc494/lib/internal/streams/readable.js#L1107
|
||||
class StreamReader {
|
||||
#ended = false
|
||||
#error
|
||||
#executor = resolve => {
|
||||
this.#resolve = resolve
|
||||
}
|
||||
#stream
|
||||
#resolve = noop
|
||||
|
||||
constructor(stream) {
|
||||
stream = typeof stream.pipe === 'function' ? stream : Readable.from(stream)
|
||||
|
||||
this.#stream = stream
|
||||
|
||||
stream.on('readable', () => this.#resolve())
|
||||
|
||||
finished(stream, { writable: false }, error => {
|
||||
this.#error = error
|
||||
this.#ended = true
|
||||
this.#resolve()
|
||||
})
|
||||
}
|
||||
|
||||
async read(size) {
|
||||
if (size !== undefined) {
|
||||
assert(size > 0)
|
||||
}
|
||||
|
||||
do {
|
||||
if (this.#ended) {
|
||||
if (this.#error) {
|
||||
throw this.#error
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const value = this.#stream.read(size)
|
||||
if (value !== null) {
|
||||
return value
|
||||
}
|
||||
|
||||
await new Promise(this.#executor)
|
||||
} while (true)
|
||||
}
|
||||
|
||||
async readStrict(size) {
|
||||
const chunk = await this.read(size)
|
||||
if (chunk === null) {
|
||||
throw new Error('stream has ended without data')
|
||||
}
|
||||
|
||||
if (size !== undefined && chunk.length !== size) {
|
||||
const error = new Error(`stream has ended with not enough data (actual: ${chunk.length}, expected: ${size})`)
|
||||
Object.defineProperties(error, {
|
||||
chunk: {
|
||||
value: chunk,
|
||||
},
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
return chunk
|
||||
}
|
||||
|
||||
async skip(size) {
|
||||
if (size === 0) {
|
||||
return size
|
||||
}
|
||||
|
||||
let toSkip = size
|
||||
do {
|
||||
if (this.#ended) {
|
||||
if (this.#error) {
|
||||
throw this.#error
|
||||
}
|
||||
return size - toSkip
|
||||
}
|
||||
|
||||
const data = this.#stream.read()
|
||||
if (data !== null) {
|
||||
toSkip -= data === null ? 0 : data.length
|
||||
if (toSkip > 0) {
|
||||
// continue to read
|
||||
} else {
|
||||
// if more than wanted has been read, push back the rest
|
||||
if (toSkip < 0) {
|
||||
this.#stream.unshift(data.slice(toSkip))
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(this.#executor)
|
||||
} while (true)
|
||||
}
|
||||
|
||||
async skipStrict(size) {
|
||||
const bytesSkipped = await this.skip(size)
|
||||
if (bytesSkipped !== size) {
|
||||
const error = new Error(`stream has ended with not enough data (actual: ${bytesSkipped}, expected: ${size})`)
|
||||
error.bytesSkipped = bytesSkipped
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StreamReader.prototype[Symbol.asyncIterator] = async function* asyncIterator() {
|
||||
let chunk
|
||||
while ((chunk = await this.read()) !== null) {
|
||||
yield chunk
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StreamReader
|
||||
141
@vates/stream-reader/index.test.js
Normal file
141
@vates/stream-reader/index.test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('node:assert').strict
|
||||
|
||||
const { Readable } = require('stream')
|
||||
|
||||
const StreamReader = require('./index.js')
|
||||
|
||||
const makeStream = it => Readable.from(it, { objectMode: false })
|
||||
makeStream.obj = Readable.from
|
||||
|
||||
const rejectionOf = promise =>
|
||||
promise.then(
|
||||
value => {
|
||||
throw value
|
||||
},
|
||||
error => error
|
||||
)
|
||||
|
||||
const makeErrorTests = method => {
|
||||
it('rejects if the stream errors', async () => {
|
||||
const error = new Error()
|
||||
const stream = makeStream([])
|
||||
|
||||
const pError = rejectionOf(new StreamReader(stream)[method](10))
|
||||
stream.destroy(error)
|
||||
|
||||
assert.strict(await pError, error)
|
||||
})
|
||||
|
||||
it('rejects if the stream has already errored', async () => {
|
||||
const error = new Error()
|
||||
const stream = makeStream([])
|
||||
|
||||
await new Promise(resolve => {
|
||||
stream.once('error', resolve).destroy(error)
|
||||
})
|
||||
|
||||
assert.strict(await rejectionOf(new StreamReader(stream)[method](10)), error)
|
||||
})
|
||||
}
|
||||
|
||||
describe('read()', () => {
|
||||
it('rejects if size is less than or equal to 0', async () => {
|
||||
const error = await rejectionOf(new StreamReader(makeStream([])).read(0))
|
||||
assert.strictEqual(error.code, 'ERR_ASSERTION')
|
||||
})
|
||||
|
||||
it('returns null if stream is empty', async () => {
|
||||
assert.strictEqual(await new StreamReader(makeStream([])).read(), null)
|
||||
})
|
||||
|
||||
makeErrorTests('read')
|
||||
|
||||
it('returns null if the stream is already ended', async () => {
|
||||
const reader = new StreamReader(makeStream([]))
|
||||
|
||||
await reader.read()
|
||||
|
||||
assert.strictEqual(await reader.read(), null)
|
||||
})
|
||||
|
||||
describe('with binary stream', () => {
|
||||
it('returns the first chunk of data', async () => {
|
||||
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(), Buffer.from('foo'))
|
||||
})
|
||||
|
||||
it('returns a chunk of the specified size (smaller than first)', async () => {
|
||||
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(2), Buffer.from('fo'))
|
||||
})
|
||||
|
||||
it('returns a chunk of the specified size (larger than first)', async () => {
|
||||
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(4), Buffer.from('foob'))
|
||||
})
|
||||
|
||||
it('returns less data if stream ends', async () => {
|
||||
assert.deepEqual(await new StreamReader(makeStream(['foo', 'bar'])).read(10), Buffer.from('foobar'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('with object stream', () => {
|
||||
it('returns the first chunk of data verbatim', async () => {
|
||||
const chunks = [{}, {}]
|
||||
assert.strictEqual(await new StreamReader(makeStream.obj(chunks)).read(), chunks[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('readStrict()', function () {
|
||||
it('throws if stream is empty', async () => {
|
||||
const error = await rejectionOf(new StreamReader(makeStream([])).readStrict())
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended without data')
|
||||
assert.strictEqual(error.chunk, undefined)
|
||||
})
|
||||
|
||||
it('throws if stream ends with not enough data', async () => {
|
||||
const error = await rejectionOf(new StreamReader(makeStream(['foo', 'bar'])).readStrict(10))
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 6, expected: 10)')
|
||||
assert.deepEqual(error.chunk, Buffer.from('foobar'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('skip()', function () {
|
||||
makeErrorTests('skip')
|
||||
|
||||
it('returns 0 if size is 0', async () => {
|
||||
assert.strictEqual(await new StreamReader(makeStream(['foo'])).skip(0), 0)
|
||||
})
|
||||
|
||||
it('returns 0 if the stream is already ended', async () => {
|
||||
const reader = new StreamReader(makeStream([]))
|
||||
|
||||
await reader.read()
|
||||
|
||||
assert.strictEqual(await reader.skip(10), 0)
|
||||
})
|
||||
|
||||
it('skips a number of bytes', async () => {
|
||||
const reader = new StreamReader(makeStream('foo bar'))
|
||||
|
||||
assert.strictEqual(await reader.skip(4), 4)
|
||||
assert.deepEqual(await reader.read(4), Buffer.from('bar'))
|
||||
})
|
||||
|
||||
it('returns less size if stream ends', async () => {
|
||||
assert.deepEqual(await new StreamReader(makeStream('foo bar')).skip(10), 7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('skipStrict()', function () {
|
||||
it('throws if stream ends with not enough data', async () => {
|
||||
const error = await rejectionOf(new StreamReader(makeStream('foo bar')).skipStrict(10))
|
||||
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 7, expected: 10)')
|
||||
assert.deepEqual(error.bytesSkipped, 7)
|
||||
})
|
||||
})
|
||||
39
@vates/stream-reader/package.json
Normal file
39
@vates/stream-reader/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/stream-reader",
|
||||
"description": "Efficiently reads and skips chunks of a given size in a stream",
|
||||
"keywords": [
|
||||
"async",
|
||||
"chunk",
|
||||
"data",
|
||||
"node",
|
||||
"promise",
|
||||
"read",
|
||||
"reader",
|
||||
"skip",
|
||||
"stream"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/stream-reader",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/stream-reader",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.3.0"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,12 @@
|
||||
import { Task } from '@vates/task'
|
||||
|
||||
const task = new Task({
|
||||
name: 'my task',
|
||||
// data in this object will be sent along the *start* event
|
||||
//
|
||||
// property names should be chosen as not to clash with properties used by `Task` or `combineEvents`
|
||||
data: {
|
||||
name: 'my task',
|
||||
},
|
||||
|
||||
// if defined, a new detached task is created
|
||||
//
|
||||
@@ -25,8 +30,19 @@ const task = new Task({
|
||||
// this field is settable once before being observed
|
||||
task.id
|
||||
|
||||
// contains the current status of the task
|
||||
//
|
||||
// possible statuses are:
|
||||
// - pending
|
||||
// - success
|
||||
// - failure
|
||||
// - aborted
|
||||
task.status
|
||||
await task.abort()
|
||||
|
||||
// Triggers the abort signal associated to the task.
|
||||
//
|
||||
// This simply requests the task to abort, it will be up to the task to handle or not this signal.
|
||||
task.abort(reason)
|
||||
|
||||
// if fn rejects, the task will be marked as failed
|
||||
const result = await task.runInside(fn)
|
||||
@@ -34,7 +50,11 @@ const result = await task.runInside(fn)
|
||||
// if fn rejects, the task will be marked as failed
|
||||
// if fn resolves, the task will be marked as succeeded
|
||||
const result = await task.run(fn)
|
||||
```
|
||||
|
||||
Inside a task:
|
||||
|
||||
```js
|
||||
// the abort signal of the current task if any, otherwise is `undefined`
|
||||
Task.abortSignal
|
||||
|
||||
@@ -52,3 +72,43 @@ Task.warning(message, data)
|
||||
// - progress
|
||||
Task.set(property, value)
|
||||
```
|
||||
|
||||
### `combineEvents`
|
||||
|
||||
Create a consolidated log from individual events.
|
||||
|
||||
It can be used directly as an `onProgress` callback:
|
||||
|
||||
```js
|
||||
import { makeOnProgress } from '@vates/task/combineEvents'
|
||||
|
||||
const onProgress = makeOnProgress({
|
||||
// This function is called each time a root task starts.
|
||||
//
|
||||
// It will be called for as many times as there are tasks created with this `onProgress` function.
|
||||
onRootTaskStart(taskLog) {
|
||||
// `taskLog` is an object reflecting the state of this task and all its subtasks,
|
||||
// and will be mutated in real-time to reflect the changes of the task.
|
||||
},
|
||||
|
||||
// This function is called each time a root task ends.
|
||||
onRootTaskEnd(taskLog) {},
|
||||
|
||||
// This function is called each time a root task or a subtask is updated.
|
||||
//
|
||||
// `taskLog.$root` can be used to uncondionally access the root task.
|
||||
onTaskUpdate(taskLog) {},
|
||||
})
|
||||
|
||||
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
|
||||
```
|
||||
|
||||
It can also be fed event logs directly:
|
||||
|
||||
```js
|
||||
import { makeOnProgress } from '@vates/task/combineEvents'
|
||||
|
||||
const onProgress = makeOnProgress({ onRootTaskStart, onRootTaskEnd, onTaskUpdate })
|
||||
|
||||
eventLogs.forEach(onProgress)
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/task):
|
||||
|
||||
```
|
||||
> npm install --save @vates/task
|
||||
```sh
|
||||
npm install --save @vates/task
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -18,7 +18,12 @@ Installation of the [npm package](https://npmjs.org/package/@vates/task):
|
||||
import { Task } from '@vates/task'
|
||||
|
||||
const task = new Task({
|
||||
name: 'my task',
|
||||
// data in this object will be sent along the *start* event
|
||||
//
|
||||
// property names should be chosen as not to clash with properties used by `Task` or `combineEvents`
|
||||
data: {
|
||||
name: 'my task',
|
||||
},
|
||||
|
||||
// if defined, a new detached task is created
|
||||
//
|
||||
@@ -41,8 +46,19 @@ const task = new Task({
|
||||
// this field is settable once before being observed
|
||||
task.id
|
||||
|
||||
// contains the current status of the task
|
||||
//
|
||||
// possible statuses are:
|
||||
// - pending
|
||||
// - success
|
||||
// - failure
|
||||
// - aborted
|
||||
task.status
|
||||
await task.abort()
|
||||
|
||||
// Triggers the abort signal associated to the task.
|
||||
//
|
||||
// This simply requests the task to abort, it will be up to the task to handle or not this signal.
|
||||
task.abort(reason)
|
||||
|
||||
// if fn rejects, the task will be marked as failed
|
||||
const result = await task.runInside(fn)
|
||||
@@ -50,7 +66,11 @@ const result = await task.runInside(fn)
|
||||
// if fn rejects, the task will be marked as failed
|
||||
// if fn resolves, the task will be marked as succeeded
|
||||
const result = await task.run(fn)
|
||||
```
|
||||
|
||||
Inside a task:
|
||||
|
||||
```js
|
||||
// the abort signal of the current task if any, otherwise is `undefined`
|
||||
Task.abortSignal
|
||||
|
||||
@@ -69,6 +89,46 @@ Task.warning(message, data)
|
||||
Task.set(property, value)
|
||||
```
|
||||
|
||||
### `combineEvents`
|
||||
|
||||
Create a consolidated log from individual events.
|
||||
|
||||
It can be used directly as an `onProgress` callback:
|
||||
|
||||
```js
|
||||
import { makeOnProgress } from '@vates/task/combineEvents'
|
||||
|
||||
const onProgress = makeOnProgress({
|
||||
// This function is called each time a root task starts.
|
||||
//
|
||||
// It will be called for as many times as there are tasks created with this `onProgress` function.
|
||||
onRootTaskStart(taskLog) {
|
||||
// `taskLog` is an object reflecting the state of this task and all its subtasks,
|
||||
// and will be mutated in real-time to reflect the changes of the task.
|
||||
},
|
||||
|
||||
// This function is called each time a root task ends.
|
||||
onRootTaskEnd(taskLog) {},
|
||||
|
||||
// This function is called each time a root task or a subtask is updated.
|
||||
//
|
||||
// `taskLog.$root` can be used to uncondionally access the root task.
|
||||
onTaskUpdate(taskLog) {},
|
||||
})
|
||||
|
||||
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
|
||||
```
|
||||
|
||||
It can also be fed event logs directly:
|
||||
|
||||
```js
|
||||
import { makeOnProgress } from '@vates/task/combineEvents'
|
||||
|
||||
const onProgress = makeOnProgress({ onRootTaskStart, onRootTaskEnd, onTaskUpdate })
|
||||
|
||||
eventLogs.forEach(onProgress)
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
|
||||
77
@vates/task/combineEvents.js
Normal file
77
@vates/task/combineEvents.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert').strict
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
function omit(source, keys, target = { __proto__: null }) {
|
||||
for (const key of Object.keys(source)) {
|
||||
if (!keys.has(key)) {
|
||||
target[key] = source[key]
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
const IGNORED_START_PROPS = new Set([
|
||||
'end',
|
||||
'infos',
|
||||
'properties',
|
||||
'result',
|
||||
'status',
|
||||
'tasks',
|
||||
'timestamp',
|
||||
'type',
|
||||
'warnings',
|
||||
])
|
||||
|
||||
exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noop, onTaskUpdate = noop }) {
|
||||
const taskLogs = new Map()
|
||||
return function onProgress(event) {
|
||||
const { id, type } = event
|
||||
let taskLog
|
||||
if (type === 'start') {
|
||||
taskLog = omit(event, IGNORED_START_PROPS)
|
||||
taskLog.start = event.timestamp
|
||||
taskLog.status = 'pending'
|
||||
taskLogs.set(id, taskLog)
|
||||
|
||||
const { parentId } = event
|
||||
if (parentId === undefined) {
|
||||
Object.defineProperty(taskLog, '$root', { value: taskLog })
|
||||
|
||||
// start of a root task
|
||||
onRootTaskStart(taskLog)
|
||||
} else {
|
||||
// start of a subtask
|
||||
const parent = taskLogs.get(parentId)
|
||||
assert.notEqual(parent, undefined)
|
||||
|
||||
// inject a (non-enumerable) reference to the parent and the root task
|
||||
Object.defineProperties(taskLog, { $parent: { value: parent }, $root: { value: parent.$root } })
|
||||
;(parent.tasks ?? (parent.tasks = [])).push(taskLog)
|
||||
}
|
||||
} else {
|
||||
taskLog = taskLogs.get(id)
|
||||
assert.notEqual(taskLog, undefined)
|
||||
|
||||
if (type === 'info' || type === 'warning') {
|
||||
const key = type + 's'
|
||||
const { data, message } = event
|
||||
;(taskLog[key] ?? (taskLog[key] = [])).push({ data, message })
|
||||
} else if (type === 'property') {
|
||||
;(taskLog.properties ?? (taskLog.properties = { __proto__: null }))[event.name] = event.value
|
||||
} else if (type === 'end') {
|
||||
taskLog.end = event.timestamp
|
||||
taskLog.result = event.result
|
||||
taskLog.status = event.status
|
||||
}
|
||||
|
||||
if (type === 'end' && taskLog.$root === taskLog) {
|
||||
onRootTaskEnd(taskLog)
|
||||
}
|
||||
}
|
||||
|
||||
onTaskUpdate(taskLog)
|
||||
}
|
||||
}
|
||||
67
@vates/task/combineEvents.test.js
Normal file
67
@vates/task/combineEvents.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert').strict
|
||||
const { describe, it } = require('test')
|
||||
|
||||
const { makeOnProgress } = require('./combineEvents.js')
|
||||
const { Task } = require('./index.js')
|
||||
|
||||
describe('makeOnProgress()', function () {
|
||||
it('works', async function () {
|
||||
const events = []
|
||||
let log
|
||||
const task = new Task({
|
||||
data: { name: 'task' },
|
||||
onProgress: makeOnProgress({
|
||||
onRootTaskStart(log_) {
|
||||
assert.equal(log, undefined)
|
||||
log = log_
|
||||
events.push('onRootTaskStart')
|
||||
},
|
||||
onRootTaskEnd(log_) {
|
||||
assert.equal(log_, log)
|
||||
events.push('onRootTaskEnd')
|
||||
},
|
||||
|
||||
onTaskUpdate(log_) {
|
||||
assert.equal(log_.$root, log)
|
||||
events.push('onTaskUpdate')
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
assert.equal(events.length, 0)
|
||||
|
||||
await task.run(async () => {
|
||||
assert.equal(events[0], 'onRootTaskStart')
|
||||
assert.equal(events[1], 'onTaskUpdate')
|
||||
assert.equal(log.name, 'task')
|
||||
|
||||
Task.set('progress', 0)
|
||||
assert.equal(events[2], 'onTaskUpdate')
|
||||
assert.equal(log.properties.progress, 0)
|
||||
|
||||
Task.info('foo', {})
|
||||
assert.equal(events[3], 'onTaskUpdate')
|
||||
assert.deepEqual(log.infos, [{ data: {}, message: 'foo' }])
|
||||
|
||||
await Task.run({ data: { name: 'subtask' } }, () => {
|
||||
assert.equal(events[4], 'onTaskUpdate')
|
||||
assert.equal(log.tasks[0].name, 'subtask')
|
||||
|
||||
Task.warning('bar', {})
|
||||
assert.equal(events[5], 'onTaskUpdate')
|
||||
assert.deepEqual(log.tasks[0].warnings, [{ data: {}, message: 'bar' }])
|
||||
})
|
||||
assert.equal(events[6], 'onTaskUpdate')
|
||||
assert.equal(log.tasks[0].status, 'success')
|
||||
|
||||
Task.set('progress', 100)
|
||||
assert.equal(events[7], 'onTaskUpdate')
|
||||
assert.equal(log.properties.progress, 100)
|
||||
})
|
||||
assert.equal(events[8], 'onRootTaskEnd')
|
||||
assert.equal(events[9], 'onTaskUpdate')
|
||||
assert.equal(log.status, 'success')
|
||||
})
|
||||
})
|
||||
@@ -11,13 +11,15 @@ function define(object, property, value) {
|
||||
const noop = Function.prototype
|
||||
|
||||
const ABORTED = 'aborted'
|
||||
const ABORTING = 'aborting'
|
||||
const FAILURE = 'failure'
|
||||
const PENDING = 'pending'
|
||||
const SUCCESS = 'success'
|
||||
exports.STATUS = { ABORTED, ABORTING, FAILURE, PENDING, SUCCESS }
|
||||
exports.STATUS = { ABORTED, FAILURE, PENDING, SUCCESS }
|
||||
|
||||
// stored in the global context so that various versions of the library can interact.
|
||||
const asyncStorageKey = '@vates/task@0'
|
||||
const asyncStorage = global[asyncStorageKey] ?? (global[asyncStorageKey] = new AsyncLocalStorage())
|
||||
|
||||
const asyncStorage = new AsyncLocalStorage()
|
||||
const getTask = () => asyncStorage.getStore()
|
||||
|
||||
exports.Task = class Task {
|
||||
@@ -66,7 +68,6 @@ exports.Task = class Task {
|
||||
|
||||
#abortController = new AbortController()
|
||||
#onProgress
|
||||
#parent
|
||||
|
||||
get id() {
|
||||
return (this.id = Math.random().toString(36).slice(2))
|
||||
@@ -82,16 +83,14 @@ exports.Task = class Task {
|
||||
return this.#status
|
||||
}
|
||||
|
||||
constructor({ name, onProgress }) {
|
||||
this.#startData = { name }
|
||||
constructor({ data = {}, onProgress } = {}) {
|
||||
this.#startData = data
|
||||
|
||||
if (onProgress !== undefined) {
|
||||
this.#onProgress = onProgress
|
||||
} else {
|
||||
const parent = getTask()
|
||||
if (parent !== undefined) {
|
||||
this.#parent = parent
|
||||
|
||||
const { signal } = parent.#abortController
|
||||
signal.addEventListener('abort', () => {
|
||||
this.#abortController.abort(signal.reason)
|
||||
@@ -106,8 +105,12 @@ exports.Task = class Task {
|
||||
|
||||
const { signal } = this.#abortController
|
||||
signal.addEventListener('abort', () => {
|
||||
if (this.status === PENDING) {
|
||||
this.#status = this.#running ? ABORTING : ABORTED
|
||||
if (this.status === PENDING && !this.#running) {
|
||||
this.#maybeStart()
|
||||
|
||||
const status = ABORTED
|
||||
this.#status = status
|
||||
this.#emit('end', { result: signal.reason, status })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -123,14 +126,12 @@ exports.Task = class Task {
|
||||
this.#onProgress(data)
|
||||
}
|
||||
|
||||
#handleMaybeAbortion(result) {
|
||||
if (this.status === ABORTING) {
|
||||
this.#status = ABORTED
|
||||
this.#emit('end', { status: ABORTED, result })
|
||||
return true
|
||||
#maybeStart() {
|
||||
const startData = this.#startData
|
||||
if (startData !== undefined) {
|
||||
this.#startData = undefined
|
||||
this.#emit('start', startData)
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async run(fn) {
|
||||
@@ -148,22 +149,19 @@ exports.Task = class Task {
|
||||
assert.equal(this.#running, false)
|
||||
this.#running = true
|
||||
|
||||
const startData = this.#startData
|
||||
if (startData !== undefined) {
|
||||
this.#startData = undefined
|
||||
this.#emit('start', startData)
|
||||
}
|
||||
this.#maybeStart()
|
||||
|
||||
try {
|
||||
const result = await asyncStorage.run(this, fn)
|
||||
this.#handleMaybeAbortion(result)
|
||||
this.#running = false
|
||||
return result
|
||||
} catch (result) {
|
||||
if (!this.#handleMaybeAbortion(result)) {
|
||||
this.#status = FAILURE
|
||||
this.#emit('end', { status: FAILURE, result })
|
||||
}
|
||||
const { signal } = this.#abortController
|
||||
const aborted = signal.aborted && result === signal.reason
|
||||
const status = aborted ? ABORTED : FAILURE
|
||||
|
||||
this.#status = status
|
||||
this.#emit('end', { status, result })
|
||||
throw result
|
||||
}
|
||||
}
|
||||
|
||||
341
@vates/task/index.test.js
Normal file
341
@vates/task/index.test.js
Normal file
@@ -0,0 +1,341 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert').strict
|
||||
const { describe, it } = require('test')
|
||||
|
||||
const { Task } = require('./index.js')
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
function assertEvent(task, expected, eventIndex = -1) {
|
||||
const logs = task.$events
|
||||
const actual = logs[eventIndex < 0 ? logs.length + eventIndex : eventIndex]
|
||||
|
||||
assert.equal(typeof actual, 'object')
|
||||
assert.equal(typeof actual.id, 'string')
|
||||
assert.equal(typeof actual.timestamp, 'number')
|
||||
for (const keys of Object.keys(expected)) {
|
||||
assert.equal(actual[keys], expected[keys])
|
||||
}
|
||||
}
|
||||
|
||||
// like new Task() but with a custom onProgress which adds event to task.$events
|
||||
function createTask(opts) {
|
||||
const events = []
|
||||
const task = new Task({ ...opts, onProgress: events.push.bind(events) })
|
||||
task.$events = events
|
||||
return task
|
||||
}
|
||||
|
||||
describe('Task', function () {
|
||||
describe('contructor', function () {
|
||||
it('data properties are passed to the start event', async function () {
|
||||
const data = { foo: 0, bar: 1 }
|
||||
const task = createTask({ data })
|
||||
await task.run(noop)
|
||||
assertEvent(task, { ...data, type: 'start' }, 0)
|
||||
})
|
||||
})
|
||||
|
||||
it('subtasks events are passed to root task', async function () {
|
||||
const task = createTask()
|
||||
const result = {}
|
||||
|
||||
await task.run(async () => {
|
||||
await new Task().run(() => result)
|
||||
})
|
||||
|
||||
assert.equal(task.$events.length, 4)
|
||||
assertEvent(task, { type: 'start', parentId: task.id }, 1)
|
||||
assertEvent(task, { type: 'end', status: 'success', result }, 2)
|
||||
})
|
||||
|
||||
describe('.abortSignal', function () {
|
||||
it('is undefined when run outside a task', function () {
|
||||
assert.equal(Task.abortSignal, undefined)
|
||||
})
|
||||
|
||||
it('is the current abort signal when run inside a task', async function () {
|
||||
const task = createTask()
|
||||
await task.run(() => {
|
||||
const { abortSignal } = Task
|
||||
assert.equal(abortSignal.aborted, false)
|
||||
task.abort()
|
||||
assert.equal(abortSignal.aborted, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.abort()', function () {
|
||||
it('aborts if the task throws fails with the abort reason', async function () {
|
||||
const task = createTask()
|
||||
const reason = {}
|
||||
|
||||
await task
|
||||
.run(() => {
|
||||
task.abort(reason)
|
||||
|
||||
Task.abortSignal.throwIfAborted()
|
||||
})
|
||||
.catch(noop)
|
||||
|
||||
assert.equal(task.status, 'aborted')
|
||||
|
||||
assert.equal(task.$events.length, 2)
|
||||
assertEvent(task, { type: 'start' }, 0)
|
||||
assertEvent(task, { type: 'end', status: 'aborted', result: reason }, 1)
|
||||
})
|
||||
|
||||
it('does not abort if the task fails without the abort reason', async function () {
|
||||
const task = createTask()
|
||||
const result = new Error()
|
||||
|
||||
await task
|
||||
.run(() => {
|
||||
task.abort({})
|
||||
|
||||
throw result
|
||||
})
|
||||
.catch(noop)
|
||||
|
||||
assert.equal(task.status, 'failure')
|
||||
|
||||
assert.equal(task.$events.length, 2)
|
||||
assertEvent(task, { type: 'start' }, 0)
|
||||
assertEvent(task, { type: 'end', status: 'failure', result }, 1)
|
||||
})
|
||||
|
||||
it('does not abort if the task succeed', async function () {
|
||||
const task = createTask()
|
||||
const result = {}
|
||||
|
||||
await task
|
||||
.run(() => {
|
||||
task.abort({})
|
||||
|
||||
return result
|
||||
})
|
||||
.catch(noop)
|
||||
|
||||
assert.equal(task.status, 'success')
|
||||
|
||||
assert.equal(task.$events.length, 2)
|
||||
assertEvent(task, { type: 'start' }, 0)
|
||||
assertEvent(task, { type: 'end', status: 'success', result }, 1)
|
||||
})
|
||||
|
||||
it('aborts before task is running', function () {
|
||||
const task = createTask()
|
||||
const reason = {}
|
||||
|
||||
task.abort(reason)
|
||||
|
||||
assert.equal(task.status, 'aborted')
|
||||
|
||||
assert.equal(task.$events.length, 2)
|
||||
assertEvent(task, { type: 'start' }, 0)
|
||||
assertEvent(task, { type: 'end', status: 'aborted', result: reason }, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.info()', function () {
|
||||
it('does nothing when run outside a task', function () {
|
||||
Task.info('foo')
|
||||
})
|
||||
|
||||
it('emits an info message when run inside a task', async function () {
|
||||
const task = createTask()
|
||||
await task.run(() => {
|
||||
Task.info('foo')
|
||||
assertEvent(task, {
|
||||
data: undefined,
|
||||
message: 'foo',
|
||||
type: 'info',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.set()', function () {
|
||||
it('does nothing when run outside a task', function () {
|
||||
Task.set('progress', 10)
|
||||
})
|
||||
|
||||
it('emits an info message when run inside a task', async function () {
|
||||
const task = createTask()
|
||||
await task.run(() => {
|
||||
Task.set('progress', 10)
|
||||
assertEvent(task, {
|
||||
name: 'progress',
|
||||
type: 'property',
|
||||
value: 10,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.warning()', function () {
|
||||
it('does nothing when run outside a task', function () {
|
||||
Task.warning('foo')
|
||||
})
|
||||
|
||||
it('emits an warning message when run inside a task', async function () {
|
||||
const task = createTask()
|
||||
await task.run(() => {
|
||||
Task.warning('foo')
|
||||
assertEvent(task, {
|
||||
data: undefined,
|
||||
message: 'foo',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#id', function () {
|
||||
it('can be set', function () {
|
||||
const task = createTask()
|
||||
task.id = 'foo'
|
||||
assert.equal(task.id, 'foo')
|
||||
})
|
||||
|
||||
it('cannot be set more than once', function () {
|
||||
const task = createTask()
|
||||
task.id = 'foo'
|
||||
|
||||
assert.throws(() => {
|
||||
task.id = 'bar'
|
||||
}, TypeError)
|
||||
})
|
||||
|
||||
it('is randomly generated if not set', function () {
|
||||
assert.notEqual(createTask().id, createTask().id)
|
||||
})
|
||||
|
||||
it('cannot be set after being observed', function () {
|
||||
const task = createTask()
|
||||
noop(task.id)
|
||||
|
||||
assert.throws(() => {
|
||||
task.id = 'bar'
|
||||
}, TypeError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#status', function () {
|
||||
it('starts as pending', function () {
|
||||
assert.equal(createTask().status, 'pending')
|
||||
})
|
||||
|
||||
it('changes to success when finish without error', async function () {
|
||||
const task = createTask()
|
||||
await task.run(noop)
|
||||
assert.equal(task.status, 'success')
|
||||
})
|
||||
|
||||
it('changes to failure when finish with error', async function () {
|
||||
const task = createTask()
|
||||
await task
|
||||
.run(() => {
|
||||
throw Error()
|
||||
})
|
||||
.catch(noop)
|
||||
assert.equal(task.status, 'failure')
|
||||
})
|
||||
|
||||
it('changes to aborted after run is complete', async function () {
|
||||
const task = createTask()
|
||||
await task
|
||||
.run(() => {
|
||||
task.abort()
|
||||
assert.equal(task.status, 'pending')
|
||||
Task.abortSignal.throwIfAborted()
|
||||
})
|
||||
.catch(noop)
|
||||
assert.equal(task.status, 'aborted')
|
||||
})
|
||||
|
||||
it('changes to aborted if aborted when not running', async function () {
|
||||
const task = createTask()
|
||||
task.abort()
|
||||
assert.equal(task.status, 'aborted')
|
||||
})
|
||||
})
|
||||
|
||||
function makeRunTests(run) {
|
||||
it('starts the task', async function () {
|
||||
const task = createTask()
|
||||
await run(task, () => {
|
||||
assertEvent(task, { type: 'start' })
|
||||
})
|
||||
})
|
||||
|
||||
it('finishes the task on success', async function () {
|
||||
const task = createTask()
|
||||
await run(task, () => 'foo')
|
||||
assert.equal(task.status, 'success')
|
||||
assertEvent(task, {
|
||||
status: 'success',
|
||||
result: 'foo',
|
||||
type: 'end',
|
||||
})
|
||||
})
|
||||
|
||||
it('fails the task on error', async function () {
|
||||
const task = createTask()
|
||||
const e = new Error()
|
||||
await run(task, () => {
|
||||
throw e
|
||||
}).catch(noop)
|
||||
|
||||
assert.equal(task.status, 'failure')
|
||||
assertEvent(task, {
|
||||
status: 'failure',
|
||||
result: e,
|
||||
type: 'end',
|
||||
})
|
||||
})
|
||||
}
|
||||
describe('.run', function () {
|
||||
makeRunTests((task, fn) => task.run(fn))
|
||||
})
|
||||
describe('.wrap', function () {
|
||||
makeRunTests((task, fn) => task.wrap(fn)())
|
||||
})
|
||||
|
||||
function makeRunInsideTests(run) {
|
||||
it('starts the task', async function () {
|
||||
const task = createTask()
|
||||
await run(task, () => {
|
||||
assertEvent(task, { type: 'start' })
|
||||
})
|
||||
})
|
||||
|
||||
it('does not finish the task on success', async function () {
|
||||
const task = createTask()
|
||||
await run(task, () => 'foo')
|
||||
assert.equal(task.status, 'pending')
|
||||
})
|
||||
|
||||
it('fails the task on error', async function () {
|
||||
const task = createTask()
|
||||
const e = new Error()
|
||||
await run(task, () => {
|
||||
throw e
|
||||
}).catch(noop)
|
||||
|
||||
assert.equal(task.status, 'failure')
|
||||
assertEvent(task, {
|
||||
status: 'failure',
|
||||
result: e,
|
||||
type: 'end',
|
||||
})
|
||||
})
|
||||
}
|
||||
describe('.runInside', function () {
|
||||
makeRunInsideTests((task, fn) => task.runInside(fn))
|
||||
})
|
||||
describe('.wrapInside', function () {
|
||||
makeRunInsideTests((task, fn) => task.wrapInside(fn)())
|
||||
})
|
||||
})
|
||||
@@ -13,11 +13,19 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.2",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./combineEvents": "./combineEvents.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/toggle-scripts):
|
||||
|
||||
```
|
||||
> npm install --save @vates/toggle-scripts
|
||||
```sh
|
||||
npm install --save @vates/toggle-scripts
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/async-map):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/async-map
|
||||
```sh
|
||||
npm install --save @xen-orchestra/async-map
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/audit-core):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/audit-core
|
||||
```sh
|
||||
npm install --save @xen-orchestra/audit-core
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups-cli):
|
||||
|
||||
```
|
||||
> npm install --global @xen-orchestra/backups-cli
|
||||
```sh
|
||||
npm install --global @xen-orchestra/backups-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.5",
|
||||
"@xen-orchestra/fs": "^3.3.1",
|
||||
"@xen-orchestra/backups": "^0.36.1",
|
||||
"@xen-orchestra/fs": "^3.3.4",
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
"lodash": "^4.17.15",
|
||||
@@ -27,7 +27,7 @@
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.6",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
||||
const Disposable = require('promise-toolbox/Disposable')
|
||||
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
||||
const pTimeout = require('promise-toolbox/timeout')
|
||||
const { compileTemplate } = require('@xen-orchestra/template')
|
||||
const { limitConcurrency } = require('limit-concurrency-decorator')
|
||||
|
||||
@@ -11,6 +12,7 @@ const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
|
||||
const { Task } = require('./Task.js')
|
||||
const { VmBackup } = require('./_VmBackup.js')
|
||||
const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
|
||||
const createStreamThrottle = require('./_createStreamThrottle.js')
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
@@ -25,6 +27,7 @@ const getAdaptersByRemote = adapters => {
|
||||
const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
getRemoteTimeout: 300e3,
|
||||
reportWhen: 'failure',
|
||||
}
|
||||
|
||||
@@ -38,6 +41,7 @@ const DEFAULT_VM_SETTINGS = {
|
||||
fullInterval: 0,
|
||||
healthCheckSr: undefined,
|
||||
healthCheckVmsWithTags: [],
|
||||
maxExportRate: 0,
|
||||
maxMergedDeltasPerRun: Infinity,
|
||||
offlineBackup: false,
|
||||
offlineSnapshot: false,
|
||||
@@ -45,6 +49,7 @@ const DEFAULT_VM_SETTINGS = {
|
||||
timeout: 0,
|
||||
useNbd: false,
|
||||
unconditionalSnapshot: false,
|
||||
validateVhdStreams: false,
|
||||
vmTimeout: 0,
|
||||
}
|
||||
|
||||
@@ -53,6 +58,13 @@ const DEFAULT_METADATA_SETTINGS = {
|
||||
retentionXoMetadata: 0,
|
||||
}
|
||||
|
||||
class RemoteTimeoutError extends Error {
|
||||
constructor(remoteId) {
|
||||
super('timeout while getting the remote ' + remoteId)
|
||||
this.remoteId = remoteId
|
||||
}
|
||||
}
|
||||
|
||||
exports.Backup = class Backup {
|
||||
constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
|
||||
this._config = config
|
||||
@@ -60,13 +72,6 @@ exports.Backup = class Backup {
|
||||
this._job = job
|
||||
this._schedule = schedule
|
||||
|
||||
this._getAdapter = Disposable.factory(function* (remoteId) {
|
||||
return {
|
||||
adapter: yield getAdapter(remoteId),
|
||||
remoteId,
|
||||
}
|
||||
})
|
||||
|
||||
this._getSnapshotNameLabel = compileTemplate(config.snapshotNameLabelTpl, {
|
||||
'{job.name}': job.name,
|
||||
'{vm.name_label}': vm => vm.name_label,
|
||||
@@ -87,6 +92,27 @@ exports.Backup = class Backup {
|
||||
|
||||
this._baseSettings = baseSettings
|
||||
this._settings = { ...baseSettings, ...job.settings[schedule.id] }
|
||||
|
||||
const { getRemoteTimeout } = this._settings
|
||||
this._getAdapter = async function (remoteId) {
|
||||
try {
|
||||
const disposable = await pTimeout.call(getAdapter(remoteId), getRemoteTimeout, new RemoteTimeoutError(remoteId))
|
||||
|
||||
return new Disposable(() => disposable.dispose(), {
|
||||
adapter: disposable.value,
|
||||
remoteId,
|
||||
})
|
||||
} catch (error) {
|
||||
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
||||
runTask(
|
||||
{
|
||||
name: 'get remote adapter',
|
||||
data: { type: 'remote', id: remoteId },
|
||||
},
|
||||
() => Promise.reject(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _runMetadataBackup() {
|
||||
@@ -132,20 +158,7 @@ exports.Backup = class Backup {
|
||||
})
|
||||
)
|
||||
),
|
||||
Disposable.all(
|
||||
remoteIds.map(id =>
|
||||
this._getAdapter(id).catch(error => {
|
||||
// See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
|
||||
runTask(
|
||||
{
|
||||
name: 'get remote adapter',
|
||||
data: { type: 'remote', id },
|
||||
},
|
||||
() => Promise.reject(error)
|
||||
)
|
||||
})
|
||||
)
|
||||
),
|
||||
Disposable.all(remoteIds.map(id => this._getAdapter(id))),
|
||||
async (pools, remoteAdapters) => {
|
||||
// remove adapters that failed (already handled)
|
||||
remoteAdapters = remoteAdapters.filter(_ => _ !== undefined)
|
||||
@@ -216,9 +229,11 @@ exports.Backup = class Backup {
|
||||
// FIXME: proper SimpleIdPattern handling
|
||||
const getSnapshotNameLabel = this._getSnapshotNameLabel
|
||||
const schedule = this._schedule
|
||||
const settings = this._settings
|
||||
|
||||
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
||||
|
||||
const config = this._config
|
||||
const settings = this._settings
|
||||
await Disposable.use(
|
||||
Disposable.all(
|
||||
extractIdsFromSimplePattern(job.srs).map(id =>
|
||||
@@ -233,19 +248,7 @@ exports.Backup = class Backup {
|
||||
})
|
||||
)
|
||||
),
|
||||
Disposable.all(
|
||||
extractIdsFromSimplePattern(job.remotes).map(id =>
|
||||
this._getAdapter(id).catch(error => {
|
||||
runTask(
|
||||
{
|
||||
name: 'get remote adapter',
|
||||
data: { type: 'remote', id },
|
||||
},
|
||||
() => Promise.reject(error)
|
||||
)
|
||||
})
|
||||
)
|
||||
),
|
||||
Disposable.all(extractIdsFromSimplePattern(job.remotes).map(id => this._getAdapter(id))),
|
||||
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
||||
async (srs, remoteAdapters, healthCheckSr) => {
|
||||
// remove adapters that failed (already handled)
|
||||
@@ -267,23 +270,35 @@ exports.Backup = class Backup {
|
||||
const allSettings = this._job.settings
|
||||
const baseSettings = this._baseSettings
|
||||
|
||||
const handleVm = vmUuid =>
|
||||
runTask({ name: 'backup VM', data: { type: 'VM', id: vmUuid } }, () =>
|
||||
Disposable.use(this._getRecord('VM', vmUuid), vm =>
|
||||
new VmBackup({
|
||||
baseSettings,
|
||||
config,
|
||||
getSnapshotNameLabel,
|
||||
healthCheckSr,
|
||||
job,
|
||||
remoteAdapters,
|
||||
schedule,
|
||||
settings: { ...settings, ...allSettings[vm.uuid] },
|
||||
srs,
|
||||
vm,
|
||||
}).run()
|
||||
)
|
||||
const handleVm = vmUuid => {
|
||||
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
||||
|
||||
return this._getRecord('VM', vmUuid).then(
|
||||
disposableVm =>
|
||||
Disposable.use(disposableVm, vm => {
|
||||
taskStart.data.name_label = vm.name_label
|
||||
return runTask(taskStart, () =>
|
||||
new VmBackup({
|
||||
baseSettings,
|
||||
config,
|
||||
getSnapshotNameLabel,
|
||||
healthCheckSr,
|
||||
job,
|
||||
remoteAdapters,
|
||||
schedule,
|
||||
settings: { ...settings, ...allSettings[vm.uuid] },
|
||||
srs,
|
||||
throttleStream,
|
||||
vm,
|
||||
}).run()
|
||||
)
|
||||
}),
|
||||
error =>
|
||||
runTask(taskStart, () => {
|
||||
throw error
|
||||
})
|
||||
)
|
||||
}
|
||||
const { concurrency } = settings
|
||||
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
const { Task } = require('./Task')
|
||||
|
||||
exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
||||
#xapi
|
||||
#restoredVm
|
||||
#timeout
|
||||
#xapi
|
||||
|
||||
constructor({ restoredVm, xapi }) {
|
||||
constructor({ restoredVm, timeout = 10 * 60 * 1000, xapi }) {
|
||||
this.#restoredVm = restoredVm
|
||||
this.#xapi = xapi
|
||||
this.#timeout = timeout
|
||||
}
|
||||
|
||||
async run() {
|
||||
@@ -23,7 +25,12 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
||||
|
||||
// remove vifs
|
||||
await Promise.all(restoredVm.$VIFs.map(vif => xapi.callAsync('VIF.destroy', vif.$ref)))
|
||||
|
||||
const waitForScript = restoredVm.tags.includes('xo-backup-health-check-xenstore')
|
||||
if (waitForScript) {
|
||||
await restoredVm.set_xenstore_data({
|
||||
'vm-data/xo-backup-health-check': 'planned',
|
||||
})
|
||||
}
|
||||
const start = new Date()
|
||||
// start Vm
|
||||
|
||||
@@ -34,7 +41,7 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
||||
false // Skip pre-boot checks?
|
||||
)
|
||||
const started = new Date()
|
||||
const timeout = 10 * 60 * 1000
|
||||
const timeout = this.#timeout
|
||||
const startDuration = started - start
|
||||
|
||||
let remainingTimeout = timeout - startDuration
|
||||
@@ -52,12 +59,52 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
||||
remainingTimeout -= running - started
|
||||
|
||||
if (remainingTimeout < 0) {
|
||||
throw new Error(`local xapi did not get Runnig state for VM ${restoredId} after ${timeout / 1000} second`)
|
||||
throw new Error(`local xapi did not get Running state for VM ${restoredId} after ${timeout / 1000} second`)
|
||||
}
|
||||
// wait for the guest tool version to be defined
|
||||
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
|
||||
timeout: remainingTimeout,
|
||||
})
|
||||
|
||||
const guestToolsReady = new Date()
|
||||
remainingTimeout -= guestToolsReady - running
|
||||
if (remainingTimeout < 0) {
|
||||
throw new Error(`local xapi did not get he guest tools check ${restoredId} after ${timeout / 1000} second`)
|
||||
}
|
||||
|
||||
if (waitForScript) {
|
||||
const startedRestoredVm = await xapi.waitObjectState(
|
||||
restoredVm.$ref,
|
||||
vm =>
|
||||
vm?.xenstore_data !== undefined &&
|
||||
(vm.xenstore_data['vm-data/xo-backup-health-check'] === 'success' ||
|
||||
vm.xenstore_data['vm-data/xo-backup-health-check'] === 'failure'),
|
||||
{
|
||||
timeout: remainingTimeout,
|
||||
}
|
||||
)
|
||||
const scriptOk = new Date()
|
||||
remainingTimeout -= scriptOk - guestToolsReady
|
||||
if (remainingTimeout < 0) {
|
||||
throw new Error(
|
||||
`Backup health check script did not update vm-data/xo-backup-health-check of ${restoredId} after ${
|
||||
timeout / 1000
|
||||
} second, got ${
|
||||
startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check']
|
||||
} instead of 'success' or 'failure'`
|
||||
)
|
||||
}
|
||||
|
||||
if (startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check'] !== 'success') {
|
||||
const message = startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check-error']
|
||||
if (message) {
|
||||
throw new Error(`Backup health check script failed with message ${message} for VM ${restoredId} `)
|
||||
} else {
|
||||
throw new Error(`Backup health check script failed for VM ${restoredId} `)
|
||||
}
|
||||
}
|
||||
Task.info('Backup health check script successfully executed')
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/backups
|
||||
```sh
|
||||
npm install --save @xen-orchestra/backups
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -209,8 +209,8 @@ class RemoteAdapter {
|
||||
|
||||
const isVhdDirectory = vhd instanceof VhdDirectory
|
||||
return isVhdDirectory
|
||||
? this.#useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
|
||||
: !this.#useVhdDirectory()
|
||||
? this.useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
|
||||
: !this.useVhdDirectory()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -321,12 +321,12 @@ class RemoteAdapter {
|
||||
return this._vhdDirectoryCompression
|
||||
}
|
||||
|
||||
#useVhdDirectory() {
|
||||
useVhdDirectory() {
|
||||
return this.handler.useVhdDirectory()
|
||||
}
|
||||
|
||||
#useAlias() {
|
||||
return this.#useVhdDirectory()
|
||||
return this.useVhdDirectory()
|
||||
}
|
||||
|
||||
async *#getDiskLegacy(diskId) {
|
||||
@@ -658,9 +658,9 @@ class RemoteAdapter {
|
||||
return path
|
||||
}
|
||||
|
||||
async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency, nbdClient } = {}) {
|
||||
async writeVhd(path, input, { checksum = true, validator = noop, writeBlockConcurrency } = {}) {
|
||||
const handler = this._handler
|
||||
if (this.#useVhdDirectory()) {
|
||||
if (this.useVhdDirectory()) {
|
||||
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
|
||||
const size = await createVhdDirectoryFromStream(handler, dataPath, input, {
|
||||
concurrency: writeBlockConcurrency,
|
||||
@@ -669,7 +669,6 @@ class RemoteAdapter {
|
||||
await input.task
|
||||
return validator.apply(this, arguments)
|
||||
},
|
||||
nbdClient,
|
||||
})
|
||||
await VhdAbstract.createAlias(handler, path, dataPath)
|
||||
return size
|
||||
@@ -720,7 +719,7 @@ class RemoteAdapter {
|
||||
|
||||
async readDeltaVmBackup(metadata, ignoredVdis) {
|
||||
const handler = this._handler
|
||||
const { vbds, vhds, vifs, vm } = metadata
|
||||
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
|
||||
const dir = dirname(metadata._filename)
|
||||
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
|
||||
|
||||
@@ -735,7 +734,7 @@ class RemoteAdapter {
|
||||
vdis,
|
||||
version: '1.0.0',
|
||||
vifs,
|
||||
vm,
|
||||
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,7 +746,49 @@ class RemoteAdapter {
|
||||
// _filename is a private field used to compute the backup id
|
||||
//
|
||||
// it's enumerable to make it cacheable
|
||||
return { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
|
||||
const metadata = { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
|
||||
|
||||
// backups created on XenServer < 7.1 via JSON in XML-RPC transports have boolean values encoded as integers, which make them unusable with more recent XAPIs
|
||||
if (typeof metadata.vm.is_a_template === 'number') {
|
||||
const properties = {
|
||||
vbds: ['bootable', 'unpluggable', 'storage_lock', 'empty', 'currently_attached'],
|
||||
vdis: [
|
||||
'sharable',
|
||||
'read_only',
|
||||
'storage_lock',
|
||||
'managed',
|
||||
'missing',
|
||||
'is_a_snapshot',
|
||||
'allow_caching',
|
||||
'metadata_latest',
|
||||
],
|
||||
vifs: ['currently_attached', 'MAC_autogenerated'],
|
||||
vm: ['is_a_template', 'is_control_domain', 'ha_always_run', 'is_a_snapshot', 'is_snapshot_from_vmpp'],
|
||||
vmSnapshot: ['is_a_template', 'is_control_domain', 'ha_always_run', 'is_snapshot_from_vmpp'],
|
||||
}
|
||||
|
||||
function fixBooleans(obj, properties) {
|
||||
properties.forEach(property => {
|
||||
if (typeof obj[property] === 'number') {
|
||||
obj[property] = obj[property] === 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const [key, propertiesInKey] of Object.entries(properties)) {
|
||||
const value = metadata[key]
|
||||
if (value !== undefined) {
|
||||
// some properties of the metadata are collections indexed by the opaqueRef
|
||||
const isCollection = Object.keys(value).some(subKey => subKey.startsWith('OpaqueRef:'))
|
||||
if (isCollection) {
|
||||
Object.values(value).forEach(subValue => fixBooleans(subValue, propertiesInKey))
|
||||
} else {
|
||||
fixBooleans(value, propertiesInKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ const groupBy = require('lodash/groupBy.js')
|
||||
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
||||
const keyBy = require('lodash/keyBy.js')
|
||||
const mapValues = require('lodash/mapValues.js')
|
||||
const vhdStreamValidator = require('vhd-lib/vhdStreamValidator.js')
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
const { decorateMethodsWith } = require('@vates/decorate-with')
|
||||
const { defer } = require('golike-defer')
|
||||
const { formatDateTime } = require('@xen-orchestra/xapi')
|
||||
const { pipeline } = require('node:stream')
|
||||
|
||||
const { DeltaBackupWriter } = require('./writers/DeltaBackupWriter.js')
|
||||
const { DeltaReplicationWriter } = require('./writers/DeltaReplicationWriter.js')
|
||||
@@ -44,6 +46,8 @@ const forkDeltaExport = deltaExport =>
|
||||
},
|
||||
})
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
class VmBackup {
|
||||
constructor({
|
||||
config,
|
||||
@@ -55,6 +59,7 @@ class VmBackup {
|
||||
schedule,
|
||||
settings,
|
||||
srs,
|
||||
throttleStream,
|
||||
vm,
|
||||
}) {
|
||||
if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
|
||||
@@ -82,6 +87,7 @@ class VmBackup {
|
||||
this._healthCheckSr = healthCheckSr
|
||||
this._jobId = job.id
|
||||
this._jobSnapshots = undefined
|
||||
this._throttleStream = throttleStream
|
||||
this._xapi = vm.$xapi
|
||||
|
||||
// Base VM for the export
|
||||
@@ -243,8 +249,19 @@ class VmBackup {
|
||||
const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
|
||||
fullVdisRequired,
|
||||
})
|
||||
// since NBD is network based, if one disk use nbd , all the disk use them
|
||||
// except the suspended VDI
|
||||
if (Object.values(deltaExport.streams).some(({ _nbd }) => _nbd)) {
|
||||
Task.info('Transfer data using NBD')
|
||||
}
|
||||
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
|
||||
|
||||
if (this._settings.validateVhdStreams) {
|
||||
deltaExport.streams = mapValues(deltaExport.streams, stream => pipeline(stream, vhdStreamValidator, noop))
|
||||
}
|
||||
|
||||
deltaExport.streams = mapValues(deltaExport.streams, this._throttleStream)
|
||||
|
||||
const timestamp = Date.now()
|
||||
|
||||
await this._callWriters(
|
||||
@@ -285,10 +302,12 @@ class VmBackup {
|
||||
|
||||
async _copyFull() {
|
||||
const { compression } = this.job
|
||||
const stream = await this._xapi.VM_export(this.exportedVm.$ref, {
|
||||
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
||||
useSnapshot: false,
|
||||
})
|
||||
const stream = this._throttleStream(
|
||||
await this._xapi.VM_export(this.exportedVm.$ref, {
|
||||
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
|
||||
useSnapshot: false,
|
||||
})
|
||||
)
|
||||
const sizeContainer = watchStreamSize(stream)
|
||||
|
||||
const timestamp = Date.now()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
require('@xen-orchestra/log/configure').catchGlobalErrors(
|
||||
require('@xen-orchestra/log').createLogger('xo:backups:worker')
|
||||
)
|
||||
const logger = require('@xen-orchestra/log').createLogger('xo:backups:worker')
|
||||
|
||||
require('@xen-orchestra/log/configure').catchGlobalErrors(logger)
|
||||
|
||||
require('@vates/cached-dns.lookup').createCachedLookup().patchGlobal()
|
||||
|
||||
@@ -20,6 +20,8 @@ const { Backup } = require('./Backup.js')
|
||||
const { RemoteAdapter } = require('./RemoteAdapter.js')
|
||||
const { Task } = require('./Task.js')
|
||||
|
||||
const { debug } = logger
|
||||
|
||||
class BackupWorker {
|
||||
#config
|
||||
#job
|
||||
@@ -122,6 +124,11 @@ decorateMethodsWith(BackupWorker, {
|
||||
]),
|
||||
})
|
||||
|
||||
const emitMessage = message => {
|
||||
debug('message emitted', { message })
|
||||
process.send(message)
|
||||
}
|
||||
|
||||
// Received message:
|
||||
//
|
||||
// Message {
|
||||
@@ -139,6 +146,8 @@ decorateMethodsWith(BackupWorker, {
|
||||
// result?: any
|
||||
// }
|
||||
process.on('message', async message => {
|
||||
debug('message received', { message })
|
||||
|
||||
if (message.action === 'run') {
|
||||
const backupWorker = new BackupWorker(message.data)
|
||||
try {
|
||||
@@ -147,7 +156,7 @@ process.on('message', async message => {
|
||||
{
|
||||
name: 'backup run',
|
||||
onLog: data =>
|
||||
process.send({
|
||||
emitMessage({
|
||||
data,
|
||||
type: 'log',
|
||||
}),
|
||||
@@ -156,13 +165,13 @@ process.on('message', async message => {
|
||||
)
|
||||
: await backupWorker.run()
|
||||
|
||||
process.send({
|
||||
emitMessage({
|
||||
type: 'result',
|
||||
result,
|
||||
status: 'success',
|
||||
})
|
||||
} catch (error) {
|
||||
process.send({
|
||||
emitMessage({
|
||||
type: 'result',
|
||||
result: error,
|
||||
status: 'failure',
|
||||
|
||||
@@ -541,7 +541,8 @@ exports.cleanVm = async function cleanVm(
|
||||
|
||||
// don't warn if the size has changed after a merge
|
||||
if (!merged && fileSystemSize !== size) {
|
||||
logWarn('incorrect backup size in metadata', {
|
||||
// FIXME: figure out why it occurs so often and, once fixed, log the real problems with `logWarn`
|
||||
console.warn('cleanVm: incorrect backup size in metadata', {
|
||||
path: metadataPath,
|
||||
actual: size ?? 'none',
|
||||
expected: fileSystemSize,
|
||||
|
||||
17
@xen-orchestra/backups/_createStreamThrottle.js
Normal file
17
@xen-orchestra/backups/_createStreamThrottle.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
const { pipeline } = require('node:stream')
|
||||
const { ThrottleGroup } = require('@kldzj/stream-throttle')
|
||||
const identity = require('lodash/identity.js')
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
module.exports = function createStreamThrottle(rate) {
|
||||
if (rate === 0) {
|
||||
return identity
|
||||
}
|
||||
const group = new ThrottleGroup({ rate })
|
||||
return function throttleStream(stream) {
|
||||
return pipeline(stream, group.createThrottle(), noop)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ const { defer } = require('golike-defer')
|
||||
|
||||
const { cancelableMap } = require('./_cancelableMap.js')
|
||||
const { Task } = require('./Task.js')
|
||||
const { pick } = require('lodash')
|
||||
const pick = require('lodash/pick.js')
|
||||
|
||||
const TAG_BASE_DELTA = 'xo:base_delta'
|
||||
exports.TAG_BASE_DELTA = TAG_BASE_DELTA
|
||||
@@ -187,11 +187,11 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
||||
|
||||
// 0. Create suspend_VDI
|
||||
let suspendVdi
|
||||
if (vmRecord.power_state === 'Suspended') {
|
||||
if (vmRecord.suspend_VDI !== undefined && vmRecord.suspend_VDI !== 'OpaqueRef:NULL') {
|
||||
const vdi = vdiRecords[vmRecord.suspend_VDI]
|
||||
if (vdi === undefined) {
|
||||
Task.warning('Suspend VDI not available for this suspended VM', {
|
||||
vm: pick(vmRecord, 'uuid', 'name_label'),
|
||||
vm: pick(vmRecord, 'uuid', 'name_label', 'suspend_VDI'),
|
||||
})
|
||||
} else {
|
||||
suspendVdi = await xapi.getRecord(
|
||||
@@ -258,6 +258,9 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
||||
$defer.onFailure(() => newVdi.$destroy())
|
||||
|
||||
await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
|
||||
if (vdi.virtual_size > newVdi.virtual_size) {
|
||||
await newVdi.$callAsync('resize', vdi.virtual_size)
|
||||
}
|
||||
} else if (vdiRef === vmRecord.suspend_VDI) {
|
||||
// suspendVDI has already created
|
||||
newVdi = suspendVdi
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const eos = require('end-of-stream')
|
||||
const { PassThrough } = require('stream')
|
||||
const { finished, PassThrough } = require('node:stream')
|
||||
|
||||
const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStreamUnpipe')
|
||||
|
||||
@@ -9,29 +8,29 @@ const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStr
|
||||
//
|
||||
// in case of error in the new readable stream, it will simply be unpiped
|
||||
// from the original one
|
||||
exports.forkStreamUnpipe = function forkStreamUnpipe(stream) {
|
||||
const { forks = 0 } = stream
|
||||
stream.forks = forks + 1
|
||||
exports.forkStreamUnpipe = function forkStreamUnpipe(source) {
|
||||
const { forks = 0 } = source
|
||||
source.forks = forks + 1
|
||||
|
||||
debug('forking', { forks: stream.forks })
|
||||
debug('forking', { forks: source.forks })
|
||||
|
||||
const proxy = new PassThrough()
|
||||
stream.pipe(proxy)
|
||||
eos(stream, error => {
|
||||
const fork = new PassThrough()
|
||||
source.pipe(fork)
|
||||
finished(source, { writable: false }, error => {
|
||||
if (error !== undefined) {
|
||||
debug('error on original stream, destroying fork', { error })
|
||||
proxy.destroy(error)
|
||||
fork.destroy(error)
|
||||
}
|
||||
})
|
||||
eos(proxy, error => {
|
||||
debug('end of stream, unpiping', { error, forks: --stream.forks })
|
||||
finished(fork, { readable: false }, error => {
|
||||
debug('end of stream, unpiping', { error, forks: --source.forks })
|
||||
|
||||
stream.unpipe(proxy)
|
||||
source.unpipe(fork)
|
||||
|
||||
if (stream.forks === 0) {
|
||||
if (source.forks === 0) {
|
||||
debug('no more forks, destroying original stream')
|
||||
stream.destroy(new Error('no more consumers for this stream'))
|
||||
source.destroy(new Error('no more consumers for this stream'))
|
||||
}
|
||||
})
|
||||
return proxy
|
||||
return fork
|
||||
}
|
||||
|
||||
@@ -94,13 +94,13 @@ In case any incoherence is detected, the file is deleted so it will be fully gen
|
||||
job.start(data: { mode: Mode, reportWhen: ReportWhen })
|
||||
├─ task.info(message: 'vms', data: { vms: string[] })
|
||||
├─ task.warning(message: string)
|
||||
├─ task.start(data: { type: 'VM', id: string })
|
||||
├─ task.start(data: { type: 'VM', id: string, name_label?: string })
|
||||
│ ├─ task.warning(message: string)
|
||||
| ├─ task.start(message: 'clean-vm')
|
||||
│ │ └─ task.end
|
||||
│ ├─ task.start(message: 'snapshot')
|
||||
│ │ └─ task.end
|
||||
│ ├─ task.start(message: 'export', data: { type: 'SR' | 'remote', id: string, isFull: boolean })
|
||||
│ ├─ task.start(message: 'export', data: { type: 'SR' | 'remote', id: string, name_label?: string, isFull: boolean })
|
||||
│ │ ├─ task.warning(message: string)
|
||||
│ │ ├─ task.start(message: 'transfer')
|
||||
│ │ │ ├─ task.warning(message: string)
|
||||
|
||||
35
@xen-orchestra/backups/docs/healthcheck/example.sh
Normal file
35
@xen-orchestra/backups/docs/healthcheck/example.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This script must be executed at the start of the machine.
|
||||
#
|
||||
# It must run as root to be able to use xenstore-read and xenstore-write
|
||||
|
||||
# fail in case of error or undefined variable
|
||||
set -eu
|
||||
|
||||
# stop there if a health check is not in progress
|
||||
if [ "$(xenstore-read vm-data/xo-backup-health-check 2>&1)" != planned ]
|
||||
then
|
||||
exit
|
||||
fi
|
||||
|
||||
# not necessary, but informs XO that this script has started which helps diagnose issues
|
||||
xenstore-write vm-data/xo-backup-health-check running
|
||||
|
||||
# put your test here
|
||||
#
|
||||
# in this example, the command `sqlite3` is used to validate the health of a database
|
||||
# and its output is captured and passed to XO via the XenStore in case of error
|
||||
if output=$(sqlite3 ~/my-database.sqlite3 .table 2>&1)
|
||||
then
|
||||
# inform XO everything is ok
|
||||
xenstore-write vm-data/xo-backup-health-check success
|
||||
else
|
||||
# inform XO there is an issue
|
||||
xenstore-write vm-data/xo-backup-health-check failure
|
||||
|
||||
# more info about the issue can be written to `vm-data/health-check-error`
|
||||
#
|
||||
# it will be shown in XO
|
||||
xenstore-write vm-data/xo-backup-health-check-error "$output"
|
||||
fi
|
||||
@@ -8,31 +8,31 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.29.5",
|
||||
"version": "0.36.1",
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
"test-integration": "node--test *.integ.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kldzj/stream-throttle": "^1.1.1",
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.4",
|
||||
"@vates/fuse-vhd": "^1.0.0",
|
||||
"@vates/nbd-client": "*",
|
||||
"@vates/nbd-client": "^1.2.0",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^3.3.1",
|
||||
"@xen-orchestra/fs": "^3.3.4",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"compare-versions": "^5.0.1",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"end-of-stream": "^1.4.4",
|
||||
"fs-extra": "^11.1.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.2.1",
|
||||
"vhd-lib": "^4.4.0",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xen-orchestra/xapi": "^1.6.1"
|
||||
"@xen-orchestra/xapi": "^2.2.0"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
|
||||
@@ -12,7 +12,7 @@ exports.runBackupWorker = function runBackupWorker(params, onLog) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = fork(PATH)
|
||||
|
||||
worker.on('exit', code => reject(new Error(`worker exited with code ${code}`)))
|
||||
worker.on('exit', (code, signal) => reject(new Error(`worker exited with code ${code} and signal ${signal}`)))
|
||||
worker.on('error', reject)
|
||||
|
||||
worker.on('message', message => {
|
||||
|
||||
@@ -7,6 +7,8 @@ const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
const { decorateClass } = require('@vates/decorate-with')
|
||||
const { defer } = require('golike-defer')
|
||||
const { dirname } = require('path')
|
||||
|
||||
const { formatFilenameDate } = require('../_filenameDate.js')
|
||||
@@ -18,11 +20,10 @@ const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
|
||||
const { checkVhd } = require('./_checkVhd.js')
|
||||
const { packUuid } = require('./_packUuid.js')
|
||||
const { Disposable } = require('promise-toolbox')
|
||||
const NbdClient = require('@vates/nbd-client')
|
||||
|
||||
const { debug, warn, info } = createLogger('xo:backups:DeltaBackupWriter')
|
||||
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
|
||||
|
||||
exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
||||
class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
||||
async checkBaseVdis(baseUuidToSrcVdi) {
|
||||
const { handler } = this._adapter
|
||||
const backup = this._backup
|
||||
@@ -133,7 +134,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
||||
}
|
||||
}
|
||||
|
||||
async _transfer({ timestamp, deltaExport }) {
|
||||
async _transfer($defer, { timestamp, deltaExport }) {
|
||||
const adapter = this._adapter
|
||||
const backup = this._backup
|
||||
|
||||
@@ -198,34 +199,12 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
||||
await checkVhd(handler, parentPath)
|
||||
}
|
||||
|
||||
const vdiRef = vm.$xapi.getObject(vdi.uuid).$ref
|
||||
|
||||
let nbdClient
|
||||
if (this._backup.config.useNbd) {
|
||||
debug('useNbd is enabled', { vdi: id, path })
|
||||
// get nbd if possible
|
||||
try {
|
||||
// this will always take the first host in the list
|
||||
const [nbdInfo] = await vm.$xapi.call('VDI.get_nbd_info', vdiRef)
|
||||
debug('got NBD info', { nbdInfo, vdi: id, path })
|
||||
nbdClient = new NbdClient(nbdInfo)
|
||||
await nbdClient.connect()
|
||||
info('NBD client ready', { vdi: id, path })
|
||||
} catch (error) {
|
||||
nbdClient = undefined
|
||||
warn('error connecting to NBD server', { error, vdi: id, path })
|
||||
}
|
||||
} else {
|
||||
debug('useNbd is disabled', { vdi: id, path })
|
||||
}
|
||||
|
||||
transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
|
||||
// no checksum for VHDs, because they will be invalidated by
|
||||
// merges and chainings
|
||||
checksum: false,
|
||||
validator: tmpPath => checkVhd(handler, tmpPath),
|
||||
writeBlockConcurrency: this._backup.config.writeBlockConcurrency,
|
||||
nbdClient,
|
||||
})
|
||||
|
||||
if (isDelta) {
|
||||
@@ -248,3 +227,6 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
||||
// TODO: run cleanup?
|
||||
}
|
||||
}
|
||||
exports.DeltaBackupWriter = decorateClass(DeltaBackupWriter, {
|
||||
_transfer: defer,
|
||||
})
|
||||
|
||||
@@ -45,11 +45,13 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
||||
data: {
|
||||
id: this._sr.uuid,
|
||||
isFull,
|
||||
name_label: this._sr.name_label,
|
||||
type: 'SR',
|
||||
},
|
||||
})
|
||||
this.transfer = task.wrapFn(this.transfer)
|
||||
this.cleanup = task.wrapFn(this.cleanup, true)
|
||||
this.cleanup = task.wrapFn(this.cleanup)
|
||||
this.healthCheck = task.wrapFn(this.healthCheck, true)
|
||||
|
||||
return task.run(() => this._prepare())
|
||||
}
|
||||
@@ -80,6 +82,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
||||
}
|
||||
|
||||
async _transfer({ timestamp, deltaExport, sizeContainers }) {
|
||||
const { _warmMigration } = this._settings
|
||||
const sr = this._sr
|
||||
const { job, scheduleId, vm } = this._backup
|
||||
|
||||
@@ -92,7 +95,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
||||
__proto__: deltaExport,
|
||||
vm: {
|
||||
...deltaExport.vm,
|
||||
tags: [...deltaExport.vm.tags, 'Continuous Replication'],
|
||||
tags: _warmMigration ? deltaExport.vm.tags : [...deltaExport.vm.tags, 'Continuous Replication'],
|
||||
},
|
||||
},
|
||||
sr
|
||||
@@ -101,11 +104,13 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
||||
size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
|
||||
}
|
||||
})
|
||||
|
||||
this._targetVmRef = targetVmRef
|
||||
const targetVm = await xapi.getRecord('VM', targetVmRef)
|
||||
|
||||
await Promise.all([
|
||||
targetVm.ha_restart_priority !== '' &&
|
||||
// warm migration does not disable HA , since the goal is to start the new VM in production
|
||||
!_warmMigration &&
|
||||
targetVm.ha_restart_priority !== '' &&
|
||||
Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
|
||||
targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
|
||||
asyncMap(['start', 'start_on'], op =>
|
||||
|
||||
@@ -21,6 +21,7 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
|
||||
name: 'export',
|
||||
data: {
|
||||
id: props.sr.uuid,
|
||||
name_label: this._sr.name_label,
|
||||
type: 'SR',
|
||||
|
||||
// necessary?
|
||||
@@ -46,7 +47,7 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
|
||||
const oldVms = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vm.uuid))
|
||||
|
||||
const deleteOldBackups = () => asyncMapSettled(oldVms, vm => xapi.VM_destroy(vm.$ref))
|
||||
const { deleteFirst } = settings
|
||||
const { deleteFirst, _warmMigration } = settings
|
||||
if (deleteFirst) {
|
||||
await deleteOldBackups()
|
||||
}
|
||||
@@ -55,14 +56,18 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
|
||||
await Task.run({ name: 'transfer' }, async () => {
|
||||
targetVmRef = await xapi.VM_import(stream, sr.$ref, vm =>
|
||||
Promise.all([
|
||||
vm.add_tags('Disaster Recovery'),
|
||||
vm.ha_restart_priority !== '' && Promise.all([vm.set_ha_restart_priority(''), vm.add_tags('HA disabled')]),
|
||||
!_warmMigration && vm.add_tags('Disaster Recovery'),
|
||||
// warm migration does not disable HA , since the goal is to start the new VM in production
|
||||
!_warmMigration &&
|
||||
vm.ha_restart_priority !== '' &&
|
||||
Promise.all([vm.set_ha_restart_priority(''), vm.add_tags('HA disabled')]),
|
||||
vm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
|
||||
])
|
||||
)
|
||||
return { size: sizeContainer.size }
|
||||
})
|
||||
|
||||
this._targetVmRef = targetVmRef
|
||||
const targetVm = await xapi.getRecord('VM', targetVmRef)
|
||||
|
||||
await Promise.all([
|
||||
|
||||
@@ -80,7 +80,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
||||
assert.notStrictEqual(
|
||||
this._metadataFileName,
|
||||
undefined,
|
||||
'Metadata file name should be defined before making a healthcheck'
|
||||
'Metadata file name should be defined before making a health check'
|
||||
)
|
||||
return Task.run(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
const { Task } = require('../Task')
|
||||
const assert = require('node:assert/strict')
|
||||
const { HealthCheckVmBackup } = require('../HealthCheckVmBackup')
|
||||
|
||||
function extractOpaqueRef(str) {
|
||||
const OPAQUE_REF_RE = /OpaqueRef:[0-9a-z-]+/
|
||||
const matches = OPAQUE_REF_RE.exec(str)
|
||||
if (!matches) {
|
||||
throw new Error('no opaque ref found')
|
||||
}
|
||||
return matches[0]
|
||||
}
|
||||
exports.MixinReplicationWriter = (BaseClass = Object) =>
|
||||
class MixinReplicationWriter extends BaseClass {
|
||||
constructor({ sr, ...rest }) {
|
||||
@@ -7,4 +19,32 @@ exports.MixinReplicationWriter = (BaseClass = Object) =>
|
||||
|
||||
this._sr = sr
|
||||
}
|
||||
|
||||
healthCheck(sr) {
|
||||
assert.notEqual(this._targetVmRef, undefined, 'A vm should have been transfered to be health checked')
|
||||
// copy VM
|
||||
return Task.run(
|
||||
{
|
||||
name: 'health check',
|
||||
},
|
||||
async () => {
|
||||
const { $xapi: xapi } = sr
|
||||
let clonedVm
|
||||
try {
|
||||
const baseVm = xapi.getObject(this._targetVmRef) ?? (await xapi.waitObject(this._targetVmRef))
|
||||
const clonedRef = await xapi
|
||||
.callAsync('VM.clone', this._targetVmRef, `Health Check - ${baseVm.name_label}`)
|
||||
.then(extractOpaqueRef)
|
||||
clonedVm = xapi.getObject(clonedRef) ?? (await xapi.waitObject(clonedRef))
|
||||
|
||||
await new HealthCheckVmBackup({
|
||||
restoredVm: clonedVm,
|
||||
xapi,
|
||||
}).run()
|
||||
} finally {
|
||||
clonedVm && (await xapi.VM_destroy(clonedVm.$ref))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cr-seed-cli):
|
||||
|
||||
```
|
||||
> npm install --global @xen-orchestra/cr-seed-cli
|
||||
```sh
|
||||
npm install --global @xen-orchestra/cr-seed-cli
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.5.1",
|
||||
"xen-api": "^1.2.2"
|
||||
"xen-api": "^1.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cron):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/cron
|
||||
```sh
|
||||
npm install --save @xen-orchestra/cron
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/defined):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/defined
|
||||
```sh
|
||||
npm install --save @xen-orchestra/defined
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/emit-async):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/emit-async
|
||||
```sh
|
||||
npm install --save @xen-orchestra/emit-async
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/fs):
|
||||
|
||||
```
|
||||
> npm install --global @xen-orchestra/fs
|
||||
```sh
|
||||
npm install --global @xen-orchestra/fs
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "3.3.1",
|
||||
"version": "3.3.4",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
|
||||
@@ -20,6 +20,7 @@
|
||||
"node": ">=14.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/abort-controller": "^3.272.0",
|
||||
"@aws-sdk/client-s3": "^3.54.0",
|
||||
"@aws-sdk/lib-storage": "^3.54.0",
|
||||
"@aws-sdk/middleware-apply-body-checksum": "^3.58.0",
|
||||
@@ -28,7 +29,7 @@
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/coalesce-calls": "^0.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@vates/read-chunk": "^1.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"bind-property-descriptor": "^2.0.0",
|
||||
@@ -50,7 +51,6 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.1.6",
|
||||
"@babel/plugin-proposal-function-bind": "^7.0.0",
|
||||
"@babel/preset-env": "^7.8.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"rimraf": "^4.1.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import through2 from 'through2'
|
||||
import { createHash } from 'crypto'
|
||||
import { defer, fromEvent } from 'promise-toolbox'
|
||||
import { invert } from 'lodash'
|
||||
import invert from 'lodash/invert.js'
|
||||
|
||||
// Format: $<algorithm>$<salt>$<encrypted>
|
||||
//
|
||||
|
||||
@@ -19,7 +19,12 @@ async function addSyncStackTrace(fn, ...args) {
|
||||
try {
|
||||
return await fn.apply(this, args)
|
||||
} catch (error) {
|
||||
error.syncStack = stackContainer.stack
|
||||
let { stack } = stackContainer
|
||||
|
||||
// remove first line which does not contain stack information, simply `Error`
|
||||
stack = stack.slice(stack.indexOf('\n') + 1)
|
||||
|
||||
error.stack = [error.stack, 'From:', stack].join('\n')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@ import {
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
UploadPartCommand,
|
||||
UploadPartCopyCommand,
|
||||
} from '@aws-sdk/client-s3'
|
||||
import { Upload } from '@aws-sdk/lib-storage'
|
||||
import { NodeHttpHandler } from '@aws-sdk/node-http-handler'
|
||||
import { getApplyMd5BodyChecksumPlugin } from '@aws-sdk/middleware-apply-body-checksum'
|
||||
import assert from 'assert'
|
||||
import { Agent as HttpAgent } from 'http'
|
||||
import { Agent as HttpsAgent } from 'https'
|
||||
import pRetry from 'promise-toolbox/retry'
|
||||
@@ -24,7 +22,6 @@ import { decorateWith } from '@vates/decorate-with'
|
||||
import { PassThrough, pipeline } from 'stream'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
import copyStreamToBuffer from './_copyStreamToBuffer.js'
|
||||
import createBufferFromStream from './_createBufferFromStream.js'
|
||||
import guessAwsRegion from './_guessAwsRegion.js'
|
||||
import RemoteHandlerAbstract from './abstract'
|
||||
import { basename, join, split } from './path'
|
||||
@@ -33,12 +30,7 @@ import { asyncEach } from '@vates/async-each'
|
||||
// endpoints https://docs.aws.amazon.com/general/latest/gr/s3.html
|
||||
|
||||
// limits: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html
|
||||
const MIN_PART_SIZE = 1024 * 1024 * 5 // 5MB
|
||||
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
|
||||
const MAX_PARTS_COUNT = 10000
|
||||
const MAX_OBJECT_SIZE = 1024 * 1024 * 1024 * 1024 * 5 // 5TB
|
||||
const IDEAL_FRAGMENT_SIZE = Math.ceil(MAX_OBJECT_SIZE / MAX_PARTS_COUNT) // the smallest fragment size that still allows a 5TB upload in 10000 fragments, about 524MB
|
||||
|
||||
const { warn } = createLogger('xo:fs:s3')
|
||||
|
||||
export default class S3Handler extends RemoteHandlerAbstract {
|
||||
@@ -198,8 +190,6 @@ export default class S3Handler extends RemoteHandlerAbstract {
|
||||
|
||||
const upload = new Upload({
|
||||
client: this._s3,
|
||||
queueSize: 1,
|
||||
partSize: IDEAL_FRAGMENT_SIZE,
|
||||
params: {
|
||||
...this._createParams(path),
|
||||
Body,
|
||||
@@ -396,138 +386,6 @@ export default class S3Handler extends RemoteHandlerAbstract {
|
||||
} while (NextContinuationToken !== undefined)
|
||||
}
|
||||
|
||||
async _write(file, buffer, position) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.fd
|
||||
}
|
||||
const uploadParams = this._createParams(file)
|
||||
let fileSize
|
||||
try {
|
||||
fileSize = +(await this._s3.send(new HeadObjectCommand(uploadParams))).ContentLength
|
||||
} catch (e) {
|
||||
if (e.name === 'NotFound') {
|
||||
fileSize = 0
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
if (fileSize < MIN_PART_SIZE) {
|
||||
const resultBuffer = Buffer.alloc(Math.max(fileSize, position + buffer.length))
|
||||
if (fileSize !== 0) {
|
||||
const result = await this._s3.send(new GetObjectCommand(uploadParams))
|
||||
await copyStreamToBuffer(result.Body, resultBuffer)
|
||||
} else {
|
||||
Buffer.alloc(0).copy(resultBuffer)
|
||||
}
|
||||
buffer.copy(resultBuffer, position)
|
||||
await this._s3.send(
|
||||
new PutObjectCommand({
|
||||
...uploadParams,
|
||||
Body: resultBuffer,
|
||||
})
|
||||
)
|
||||
return { buffer, bytesWritten: buffer.length }
|
||||
} else {
|
||||
// using this trick: https://stackoverflow.com/a/38089437/72637
|
||||
// multipart fragments have a minimum size of 5Mo and a max of 5Go unless they are last
|
||||
// splitting the file in 3 parts: [prefix, edit, suffix]
|
||||
// if `prefix` is bigger than 5Mo, it will be sourced from uploadPartCopy()
|
||||
// otherwise otherwise it will be downloaded, concatenated to `edit`
|
||||
// `edit` will always be an upload part
|
||||
// `suffix` will always be sourced from uploadPartCopy()
|
||||
// Then everything will be sliced in 5Gb parts before getting uploaded
|
||||
const multipartParams = await this._s3.send(new CreateMultipartUploadCommand(uploadParams))
|
||||
const copyMultipartParams = {
|
||||
...multipartParams,
|
||||
CopySource: this._makeCopySource(file),
|
||||
}
|
||||
try {
|
||||
const parts = []
|
||||
const prefixSize = position
|
||||
let suffixOffset = prefixSize + buffer.length
|
||||
let suffixSize = Math.max(0, fileSize - suffixOffset)
|
||||
let hasSuffix = suffixSize > 0
|
||||
let editBuffer = buffer
|
||||
let editBufferOffset = position
|
||||
let partNumber = 1
|
||||
let prefixPosition = 0
|
||||
// use floor() so that last fragment is handled in the if bellow
|
||||
let fragmentsCount = Math.floor(prefixSize / MAX_PART_SIZE)
|
||||
const prefixFragmentSize = MAX_PART_SIZE
|
||||
let prefixLastFragmentSize = prefixSize - prefixFragmentSize * fragmentsCount
|
||||
if (prefixLastFragmentSize >= MIN_PART_SIZE) {
|
||||
// the last fragment of the prefix is smaller than MAX_PART_SIZE, but bigger than the minimum
|
||||
// so we can copy it too
|
||||
fragmentsCount++
|
||||
prefixLastFragmentSize = 0
|
||||
}
|
||||
for (let i = 0; i < fragmentsCount; i++) {
|
||||
const fragmentEnd = Math.min(prefixPosition + prefixFragmentSize, prefixSize)
|
||||
assert.strictEqual(fragmentEnd - prefixPosition <= MAX_PART_SIZE, true)
|
||||
const range = `bytes=${prefixPosition}-${fragmentEnd - 1}`
|
||||
const copyPrefixParams = { ...copyMultipartParams, PartNumber: partNumber++, CopySourceRange: range }
|
||||
const part = await this._s3.send(new UploadPartCopyCommand(copyPrefixParams))
|
||||
parts.push({ ETag: part.CopyPartResult.ETag, PartNumber: copyPrefixParams.PartNumber })
|
||||
prefixPosition += prefixFragmentSize
|
||||
}
|
||||
if (prefixLastFragmentSize) {
|
||||
// grab everything from the prefix that was too small to be copied, download and merge to the edit buffer.
|
||||
const downloadParams = { ...uploadParams, Range: `bytes=${prefixPosition}-${prefixSize - 1}` }
|
||||
let prefixBuffer
|
||||
if (prefixSize > 0) {
|
||||
const result = await this._s3.send(new GetObjectCommand(downloadParams))
|
||||
prefixBuffer = await createBufferFromStream(result.Body)
|
||||
} else {
|
||||
prefixBuffer = Buffer.alloc(0)
|
||||
}
|
||||
editBuffer = Buffer.concat([prefixBuffer, buffer])
|
||||
editBufferOffset -= prefixLastFragmentSize
|
||||
}
|
||||
if (hasSuffix && editBuffer.length < MIN_PART_SIZE) {
|
||||
// the edit fragment is too short and is not the last fragment
|
||||
// let's steal from the suffix fragment to reach the minimum size
|
||||
// the suffix might be too short and itself entirely absorbed in the edit fragment, making it the last one.
|
||||
const complementSize = Math.min(MIN_PART_SIZE - editBuffer.length, suffixSize)
|
||||
const complementOffset = editBufferOffset + editBuffer.length
|
||||
suffixOffset += complementSize
|
||||
suffixSize -= complementSize
|
||||
hasSuffix = suffixSize > 0
|
||||
const prefixRange = `bytes=${complementOffset}-${complementOffset + complementSize - 1}`
|
||||
const downloadParams = { ...uploadParams, Range: prefixRange }
|
||||
const result = await this._s3.send(new GetObjectCommand(downloadParams))
|
||||
const complementBuffer = await createBufferFromStream(result.Body)
|
||||
editBuffer = Buffer.concat([editBuffer, complementBuffer])
|
||||
}
|
||||
const editParams = { ...multipartParams, Body: editBuffer, PartNumber: partNumber++ }
|
||||
const editPart = await this._s3.send(new UploadPartCommand(editParams))
|
||||
parts.push({ ETag: editPart.ETag, PartNumber: editParams.PartNumber })
|
||||
if (hasSuffix) {
|
||||
// use ceil because the last fragment can be arbitrarily small.
|
||||
const suffixFragments = Math.ceil(suffixSize / MAX_PART_SIZE)
|
||||
let suffixFragmentOffset = suffixOffset
|
||||
for (let i = 0; i < suffixFragments; i++) {
|
||||
const fragmentEnd = suffixFragmentOffset + MAX_PART_SIZE
|
||||
assert.strictEqual(Math.min(fileSize, fragmentEnd) - suffixFragmentOffset <= MAX_PART_SIZE, true)
|
||||
const suffixRange = `bytes=${suffixFragmentOffset}-${Math.min(fileSize, fragmentEnd) - 1}`
|
||||
const copySuffixParams = { ...copyMultipartParams, PartNumber: partNumber++, CopySourceRange: suffixRange }
|
||||
const suffixPart = (await this._s3.send(new UploadPartCopyCommand(copySuffixParams))).CopyPartResult
|
||||
parts.push({ ETag: suffixPart.ETag, PartNumber: copySuffixParams.PartNumber })
|
||||
suffixFragmentOffset = fragmentEnd
|
||||
}
|
||||
}
|
||||
await this._s3.send(
|
||||
new CompleteMultipartUploadCommand({
|
||||
...multipartParams,
|
||||
MultipartUpload: { Parts: parts },
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
await this._s3.send(new AbortMultipartUploadCommand(multipartParams))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _openFile(path, flags) {
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
- Display network throughput chart in pool dashboard (PR [#6610](https://github.com/vatesfr/xen-orchestra/pull/6610))
|
||||
- Display RAM usage chart in pool dashboard (PR [#6604](https://github.com/vatesfr/xen-orchestra/pull/6604))
|
||||
- Ability to change the state of a VM (PRs [#6571](https://github.com/vatesfr/xen-orchestra/pull/6571) [#6608](https://github.com/vatesfr/xen-orchestra/pull/6608))
|
||||
- Display CPU provisioning in pool dashboard (PR [#6601](https://github.com/vatesfr/xen-orchestra/pull/6601))
|
||||
- Add a star icon near the pool master (PR [#6712](https://github.com/vatesfr/xen-orchestra/pull/6712))
|
||||
- Display an error message if the data cannot be fetched (PR [#6525](https://github.com/vatesfr/xen-orchestra/pull/6525))
|
||||
- Add "Under Construction" views (PR [#6673](https://github.com/vatesfr/xen-orchestra/pull/6673))
|
||||
|
||||
## **0.1.0**
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ Use the `busy` prop to display a loader icon.
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
</script>
|
||||
```
|
||||
|
||||
406
@xen-orchestra/lite/docs/component-stories.md
Normal file
406
@xen-orchestra/lite/docs/component-stories.md
Normal file
@@ -0,0 +1,406 @@
|
||||
<!-- TOC -->
|
||||
|
||||
- [Component Stories](#component-stories)
|
||||
- [How to create a story](#how-to-create-a-story)
|
||||
- [How to write a story](#how-to-write-a-story)
|
||||
_ [Example](#example)
|
||||
_ [Props](#props)
|
||||
_ [Required prop](#required-prop)
|
||||
_ [Prop type](#prop-type)
|
||||
_ [String](#string)
|
||||
_ [Number](#number)
|
||||
_ [Boolean](#boolean)
|
||||
_ [Array](#array)
|
||||
_ [Object](#object)
|
||||
_ [Enum](#enum)
|
||||
_ [Any](#any)
|
||||
_ [Custom type](#custom-type)
|
||||
_ [Prop widget](#prop-widget)
|
||||
_ [Text](#text)
|
||||
_ [Number](#number-1)
|
||||
_ [Object](#object-1)
|
||||
_ [Choice](#choice)
|
||||
_ [Boolean](#boolean-1)
|
||||
_ [Prop default](#prop-default)
|
||||
_ [Prop preset](#prop-preset)
|
||||
_ [Prop help](#prop-help)
|
||||
_ [Events](#events)
|
||||
_ [Event with no arguments](#event-with-no-arguments)
|
||||
_ [Event with arguments](#event-with-arguments)
|
||||
_ [Custom function](#custom-function)
|
||||
_ [Event type](#event-type)
|
||||
_ [Models](#models)
|
||||
_ [Default model](#default-model)
|
||||
_ [Custom model](#custom-model)
|
||||
_ [Configure the underlying prop and event](#configure-the-underlying-prop-and-event)
|
||||
_ [Model type](#model-type)
|
||||
_ [Model help](#model-help)
|
||||
_ [Slots](#slots)
|
||||
_ [Default slot](#default-slot)
|
||||
_ [Named slot](#named-slot)
|
||||
_ [Scoped slot (slot with props)](#scoped-slot--slot-with-props-)
|
||||
_ [Slot help](#slot-help)
|
||||
_ [Settings](#settings)
|
||||
<!-- TOC -->
|
||||
|
||||
# Component Stories
|
||||
|
||||
The `ComponentStory` component allows you to document your components and their props, events and slots.
|
||||
|
||||
It takes a `params` prop which is an array of configuration items.
|
||||
|
||||
You can configure props, events, models, slots and settings.
|
||||
|
||||
Props, Events and Models will be added to the `properties` slot prop.
|
||||
|
||||
Slots are only for documentation purpose.
|
||||
|
||||
Settings will be added to the `settings` slot prop.
|
||||
|
||||
## How to create a story
|
||||
|
||||
1. Create a new story component in the `src/stories` directory (ie. `my-component.story.vue`).
|
||||
2. To document your component, create the same file with the `.md` extension (ie. `my-component.story.md`).
|
||||
|
||||
## How to write a story
|
||||
|
||||
In your `.story.vue` file, import and use the `ComponentStory` component.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop(...),
|
||||
event(...),
|
||||
model(...),
|
||||
slot(...),
|
||||
setting(...),
|
||||
]"
|
||||
>
|
||||
<MyComponent v-bind="properties">
|
||||
{{ settings.label }}
|
||||
</MyComponent>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MyComponent from "@/components/MyComponent.vue";
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import { prop, event, model, slot, setting } from "@/libs/story/story-param";
|
||||
</script>
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
Let's take this Vue component:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<div>Required string prop: {{ imString }}</div>
|
||||
<div>Required number prop: {{ imNumber }}</div>
|
||||
<div v-if="imOptional">Optional prop: {{ imOptional }}</div>
|
||||
<div>Optional prop with default: {{ imOptionalWithDefault }}</div>
|
||||
<button @click="handleClick">Click me</button>
|
||||
<button @click="handleClickWithArg('some-id')">Click me with an id</button>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<div>
|
||||
<slot name="named-slot" />
|
||||
</div>
|
||||
<div>
|
||||
<slot :moon-distance="moonDistance" name="named-scoped-slot" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
imString: string;
|
||||
imNumber: number;
|
||||
imOptional?: string;
|
||||
imOptionalWithDefault?: string;
|
||||
modelValue?: string;
|
||||
customModel?: number;
|
||||
}>(),
|
||||
{ imOptionalWithDefault: "Hi World" }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "click"): void;
|
||||
(event: "clickWithArg", id: string): void;
|
||||
(event: "update:modelValue", value: string): void;
|
||||
(event: "update:customModel", value: number): void;
|
||||
}>();
|
||||
|
||||
const moonDistance = 384400;
|
||||
|
||||
const handleClick = () => emit("click");
|
||||
const handleClickWithArg = (id: string) => emit("clickWithArg", id);
|
||||
</script>
|
||||
```
|
||||
|
||||
Here is how to document it with a Component Story:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
:params="[
|
||||
prop('imString')
|
||||
.str()
|
||||
.required()
|
||||
.preset('Example')
|
||||
.widget()
|
||||
.help('This is a required string prop'),
|
||||
prop('imNumber')
|
||||
.num()
|
||||
.required()
|
||||
.preset(42)
|
||||
.widget()
|
||||
.help('This is a required number prop'),
|
||||
prop('imOptional').str().widget().help('This is an optional string prop'),
|
||||
prop('imOptionalWithDefault')
|
||||
.str()
|
||||
.default('Hi World')
|
||||
.widget()
|
||||
.default('My default value'),
|
||||
model().prop((p) => p.str()),
|
||||
model('customModel').prop((p) => p.num()),
|
||||
event('click').help('Emitted when the user clicks the first button'),
|
||||
event('clickWithArg')
|
||||
.args({ id: 'string' })
|
||||
.help('Emitted when the user clicks the second button'),
|
||||
slot().help('This is the default slot'),
|
||||
slot('namedSlot').help('This is a named slot'),
|
||||
slot('namedScopedSlot')
|
||||
.prop('moon-distance', 'number')
|
||||
.help('This is a named slot'),
|
||||
setting('contentExample').widget(text()).preset('Some content'),
|
||||
]"
|
||||
>
|
||||
<MyComponent v-bind="properties">
|
||||
{{ settings.contentExample }}
|
||||
<template #named-slot>Named slot content</template>
|
||||
<template #named-scoped-slot="{ moonDistance }">
|
||||
Moon distance is {{ moonDistance }} meters.
|
||||
</template>
|
||||
</MyComponent>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import MyComponent from "@/components/MyComponent.vue";
|
||||
import { event, model, prop, setting, slot } from "@/libs/story/story-param";
|
||||
import { text } from "@/libs/story/story-widget";
|
||||
</script>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
Use the `prop(name: string)` function to document a prop.
|
||||
|
||||
It will appear on the **Props** tab.
|
||||
|
||||
#### Required prop
|
||||
|
||||
If the prop is required, use the `required()` function.
|
||||
|
||||
`prop('title').required()`
|
||||
|
||||
#### Prop type
|
||||
|
||||
You can set the type of the prop with the `str()`, `num()`, `bool()`, `arr()`, `obj()`, `enum()` and `any()` functions.
|
||||
|
||||
The type can also be detected automatically if a [preset](#prop-preset) value is defined.
|
||||
|
||||
##### String
|
||||
|
||||
`prop('title').str()`: `string`
|
||||
|
||||
##### Number
|
||||
|
||||
`prop('count').num()`: `number`
|
||||
|
||||
##### Boolean
|
||||
|
||||
`prop('disabled').bool()`: `boolean`
|
||||
|
||||
##### Array
|
||||
|
||||
`prop('items').arr()`: `any[]`
|
||||
|
||||
`prop('items').arr('string')`: `string[]`
|
||||
|
||||
##### Object
|
||||
|
||||
`prop('user').obj()`: `object`
|
||||
|
||||
`prop('user').obj('{ name: string, age: number }')`: `{ name: string; age: number; }`
|
||||
|
||||
##### Enum
|
||||
|
||||
`prop('color').enum('red', 'green', 'blue')`: `"red" | "green" | "blue"`
|
||||
|
||||
##### Any
|
||||
|
||||
`prop('color').any()`: `any`
|
||||
|
||||
##### Custom type
|
||||
|
||||
`prop('user').type('User')`: `User`
|
||||
|
||||
#### Prop widget
|
||||
|
||||
When the prop type is defined, the widget is automatically detected.
|
||||
|
||||
`prop('title').str().widget()`
|
||||
|
||||
But you can also define the widget manually.
|
||||
|
||||
##### Text
|
||||
|
||||
`prop('...').widget(text())`
|
||||
|
||||
##### Number
|
||||
|
||||
`prop('...').widget(number())`
|
||||
|
||||
##### Object
|
||||
|
||||
`prop('...').widget(object())`
|
||||
|
||||
##### Choice
|
||||
|
||||
`prop('...').widget(choice('red', 'green', 'blue'))`
|
||||
|
||||
##### Boolean
|
||||
|
||||
`prop('title').widget(boolean())`
|
||||
|
||||
#### Prop default
|
||||
|
||||
This documents the default value of the prop, which is applied when the prop is not defined.
|
||||
|
||||
`prop('color').default('blue')`
|
||||
|
||||
#### Prop preset
|
||||
|
||||
This allows to preset a prop value for this story.
|
||||
|
||||
`prop('color').preset('red')`
|
||||
|
||||
#### Prop help
|
||||
|
||||
This allows to add a help text for this prop.
|
||||
|
||||
`prop('color').help('This is the component text color')`
|
||||
|
||||
### Events
|
||||
|
||||
Use the `event(name: string)` function to document an event.
|
||||
|
||||
It will appear in the **Events** tab.
|
||||
|
||||
When triggered, this event will be logged to the `Logs` card.
|
||||
|
||||
#### Event with no arguments
|
||||
|
||||
`event('edit')`: `() => void`
|
||||
|
||||
#### Event with arguments
|
||||
|
||||
`event('delete').args({ id: 'string' })`: `(id: string) => void`
|
||||
|
||||
#### Custom function
|
||||
|
||||
If needed, thanks to the `preset` method, you can attach a custom function to your event.
|
||||
|
||||
`const debug = (id: string) => console.log(id);`
|
||||
|
||||
`event('my-event').args({ id: 'string' }).preset(debug)`
|
||||
|
||||
#### Event type
|
||||
|
||||
The event type is automatically generated from the arguments.
|
||||
|
||||
You can override it with the `type()` method.
|
||||
|
||||
#### Event help
|
||||
|
||||
This allows to add a help text for this event.
|
||||
|
||||
`event('close').help('Called when user clicks the close icon or on the background')`
|
||||
|
||||
### Models
|
||||
|
||||
Use the `model(name = "model-value")` function to document a model.
|
||||
|
||||
Calling `model("foo")` is kind of equivalent to calling `prop("foo")` + `event("update:foo")`.
|
||||
|
||||
#### Default model
|
||||
|
||||
`model()` with no argument will create a `model-value` prop and a `update:model-value` event.
|
||||
|
||||
#### Custom model
|
||||
|
||||
`model('foo')` will create a `foo` prop and a `update:foo` event.
|
||||
|
||||
#### Configure the underlying prop and event
|
||||
|
||||
You can use `.prop((p) => ...)` and `.event((e) => ...)` methods to access the underlying prop and event respectively
|
||||
then use any of the [prop](#props) and [event](#events) methods.
|
||||
|
||||
`model().event((e) => e.help('Help for update:modelValue event'))`
|
||||
|
||||
#### Model type
|
||||
|
||||
`.type(type: string)` function is a shortcut for `.prop((p) => p.type(...))`
|
||||
|
||||
#### Model help
|
||||
|
||||
Using `.help(text: string)` function is a shortcut for `.prop((p) => p.help(...))`
|
||||
|
||||
### Slots
|
||||
|
||||
Use the `slot(name = "default")` function to document a slot.
|
||||
|
||||
#### Default slot
|
||||
|
||||
`slot()`
|
||||
|
||||
=> `<slot />`
|
||||
|
||||
#### Named slot
|
||||
|
||||
`slot('header')`
|
||||
|
||||
=> `<slot name="header" />`
|
||||
|
||||
#### Scoped slot (slot with props)
|
||||
|
||||
`slot('footer').prop('color', 'string').prop('count', 'number')`
|
||||
|
||||
#### Slot help
|
||||
|
||||
`slot('footer').help('This is the footer slot')`
|
||||
|
||||
### Settings
|
||||
|
||||
Use the `setting(name: string)` to configure your Story with arbitrary settings.
|
||||
|
||||
They will not be passed automatically to your component, but you can access them in your template with the `settings` variable.
|
||||
|
||||
For example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ComponentStory v-slot="{ settings }" :params="[setting('label').widget()]">
|
||||
<button>{{ settings.label }}</button>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
```
|
||||
1
@xen-orchestra/lite/env.d.ts
vendored
1
@xen-orchestra/lite/env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="json-rpc-2.0/dist" />
|
||||
/// <reference types="vite-plugin-pages/client" />
|
||||
|
||||
declare const XO_LITE_VERSION: string;
|
||||
declare const XO_LITE_GIT_HEAD: string;
|
||||
|
||||
@@ -18,18 +18,22 @@
|
||||
"@novnc/novnc": "^1.3.0",
|
||||
"@types/d3-time-format": "^4.0.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@vueuse/core": "^9.5.0",
|
||||
"@vueuse/math": "^9.5.0",
|
||||
"complex-matcher": "^0.7.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"echarts": "^5.3.3",
|
||||
"human-format": "^1.0.0",
|
||||
"highlight.js": "^11.6.0",
|
||||
"human-format": "^1.1.0",
|
||||
"iterable-backoff": "^0.1.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json5": "^2.2.1",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"make-error": "^1.3.6",
|
||||
"marked": "^4.2.12",
|
||||
"pinia": "^2.0.14",
|
||||
"placement.js": "^1.0.0-beta.5",
|
||||
"vue": "^3.2.37",
|
||||
@@ -53,6 +57,7 @@
|
||||
"postcss-nested": "^6.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.4",
|
||||
"vite-plugin-pages": "^0.27.1",
|
||||
"vue-tsc": "^1.0.9"
|
||||
},
|
||||
"private": true,
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
<template>
|
||||
<UiModal
|
||||
v-if="isSslModalOpen"
|
||||
color="error"
|
||||
:icon="faServer"
|
||||
@close="clearUnreachableHostsUrls"
|
||||
>
|
||||
<template #title>{{ $t("unreachable-hosts") }}</template>
|
||||
<template #subtitle>{{ $t("following-hosts-unreachable") }}</template>
|
||||
<p>{{ $t("allow-self-signed-ssl") }}</p>
|
||||
<ul>
|
||||
<li v-for="url in unreachableHostsUrls" :key="url.hostname">
|
||||
<a :href="url.href" target="_blank" rel="noopener">{{ url.href }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<template #buttons>
|
||||
<UiButton color="success" @click="reload">{{
|
||||
$t("unreachable-hosts-reload-page")
|
||||
}}</UiButton>
|
||||
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
<div v-if="!xenApiStore.isConnected">
|
||||
<UnreachableHostsModal />
|
||||
<div v-if="!$route.meta.hasStoryNav && !xenApiStore.isConnected">
|
||||
<AppLogin />
|
||||
</div>
|
||||
<div v-else>
|
||||
<AppHeader />
|
||||
<div style="display: flex">
|
||||
<transition name="slide">
|
||||
<AppNavigation />
|
||||
</transition>
|
||||
<AppNavigation />
|
||||
<main class="main">
|
||||
<RouterView />
|
||||
</main>
|
||||
@@ -38,27 +16,23 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AppNavigation from "@/components/AppNavigation.vue";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
|
||||
import { logicAnd } from "@vueuse/math";
|
||||
import { difference } from "lodash";
|
||||
import { computed, ref, watch, watchEffect } from "vue";
|
||||
import favicon from "@/assets/favicon.svg";
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import AppHeader from "@/components/AppHeader.vue";
|
||||
import AppLogin from "@/components/AppLogin.vue";
|
||||
import AppNavigation from "@/components/AppNavigation.vue";
|
||||
import AppTooltips from "@/components/AppTooltips.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import UnreachableHostsModal from "@/components/UnreachableHostsModal.vue";
|
||||
import { useChartTheme } from "@/composables/chart-theme.composable";
|
||||
import { useHostStore } from "@/stores/host.store";
|
||||
import { usePoolStore } from "@/stores/pool.store";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
|
||||
import { logicAnd } from "@vueuse/math";
|
||||
import { computed } from "vue";
|
||||
|
||||
const unreachableHostsUrls = ref<URL[]>([]);
|
||||
const clearUnreachableHostsUrls = () => (unreachableHostsUrls.value = []);
|
||||
|
||||
let link: HTMLLinkElement | null = document.querySelector("link[rel~='icon']");
|
||||
let link = document.querySelector(
|
||||
"link[rel~='icon']"
|
||||
) as HTMLLinkElement | null;
|
||||
if (link == null) {
|
||||
link = document.createElement("link");
|
||||
link.rel = "icon";
|
||||
@@ -69,7 +43,7 @@ link.href = favicon;
|
||||
document.title = "XO Lite";
|
||||
|
||||
const xenApiStore = useXenApiStore();
|
||||
const hostStore = useHostStore();
|
||||
const { pool } = usePoolStore().subscribe();
|
||||
useChartTheme();
|
||||
const uiStore = useUiStore();
|
||||
|
||||
@@ -91,47 +65,25 @@ if (import.meta.env.DEV) {
|
||||
);
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (xenApiStore.isConnected) {
|
||||
xenApiStore.init();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => hostStore.allRecords,
|
||||
(hosts, previousHosts) => {
|
||||
difference(hosts, previousHosts).forEach((host) => {
|
||||
const url = new URL("http://localhost");
|
||||
url.protocol = window.location.protocol;
|
||||
url.hostname = host.address;
|
||||
fetch(url, { mode: "no-cors" }).catch(() =>
|
||||
unreachableHostsUrls.value.push(url)
|
||||
);
|
||||
});
|
||||
whenever(
|
||||
() => pool.value?.$ref,
|
||||
async (poolRef) => {
|
||||
const xenApi = xenApiStore.getXapi();
|
||||
await xenApi.injectWatchEvent(poolRef);
|
||||
await xenApi.startWatch();
|
||||
}
|
||||
);
|
||||
|
||||
const isSslModalOpen = computed(() => unreachableHostsUrls.value.length > 0);
|
||||
const reload = () => window.location.reload();
|
||||
</script>
|
||||
|
||||
<style lang="postcss">
|
||||
@import "@/assets/base.css";
|
||||
</style>
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
transform: translateX(-37rem);
|
||||
}
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.main {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
height: calc(100vh - 9rem);
|
||||
height: calc(100vh - 8rem);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,8 +16,10 @@ a {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
code,
|
||||
code * {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
||||
.card-view {
|
||||
@@ -25,3 +27,22 @@ code {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
color: var(--color-extra-blue-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--color-extra-blue-d20);
|
||||
}
|
||||
|
||||
.link:active,
|
||||
.link.router-link-active {
|
||||
color: var(--color-extra-blue-d40);
|
||||
}
|
||||
|
||||
.link.router-link-active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
136
@xen-orchestra/lite/src/assets/color-mode-auto.svg
Normal file
136
@xen-orchestra/lite/src/assets/color-mode-auto.svg
Normal file
@@ -0,0 +1,136 @@
|
||||
<svg width="200" height="200" viewBox="3 2 200 200" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ddd_1994_118844)">
|
||||
<g clip-path="url(#clip0_1994_118844)">
|
||||
<g filter="url(#filter1_ddd_1994_118844)">
|
||||
<g clip-path="url(#clip1_1994_118844)">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="#F6F6F7"/>
|
||||
<rect width="200" height="29" transform="translate(3 2)" fill="#F6F6F7"/>
|
||||
<rect x="13" y="13" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect width="68" height="171" transform="translate(3 31)" fill="white"/>
|
||||
<rect x="13" y="41" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="41" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="58" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="58" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="75" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="75" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="92" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="92" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="109" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="109" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="126" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="126" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="143" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="143" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="160" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="160" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="177" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="177" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="194" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="194" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="76" y="36" width="122" height="44" rx="4" fill="white"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="76" y="90" width="122" height="44" rx="4" fill="white"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="76" y="144" width="122" height="44" rx="4" fill="white"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="76" y="198" width="122" height="44" rx="4" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g clip-path="url(#clip2_1994_118844)">
|
||||
<g filter="url(#filter2_ddd_1994_118844)">
|
||||
<g clip-path="url(#clip3_1994_118844)">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="#17182B"/>
|
||||
<rect width="200" height="29" transform="translate(3 2)" fill="#17182B"/>
|
||||
<rect x="76" y="36" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="76" y="90" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="76" y="144" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="76" y="198" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ddd_1994_118844" x="0" y="0" width="206" height="206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.08 0"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.06 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1994_118844" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_ddd_1994_118844" x="0" y="0" width="206" height="206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.08 0"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.06 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1994_118844" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_ddd_1994_118844" x="0" y="0" width="206" height="206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1994_118844"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1994_118844" result="effect2_dropShadow_1994_118844"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1994_118844" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_1994_118844">
|
||||
<rect width="100" height="200" fill="white" transform="translate(3 2)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1994_118844">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_1994_118844">
|
||||
<rect width="100" height="200" fill="white" transform="translate(103 2)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip3_1994_118844">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.0 KiB |
70
@xen-orchestra/lite/src/assets/color-mode-dark.svg
Normal file
70
@xen-orchestra/lite/src/assets/color-mode-dark.svg
Normal file
@@ -0,0 +1,70 @@
|
||||
<svg width="200" height="200" viewBox="3 2 200 200" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ddd_1994_118694)">
|
||||
<g clip-path="url(#clip0_1994_118694)">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="#17182B"/>
|
||||
<rect width="200" height="29" transform="translate(3 2)" fill="#17182B"/>
|
||||
<rect x="13" y="13" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect width="68" height="171" transform="translate(3 31)" fill="#14141E"/>
|
||||
<rect x="13" y="41" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="41" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="58" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="58" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="75" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="75" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="92" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="92" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="109" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="109" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="126" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="126" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="143" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="143" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="160" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="160" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="177" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="177" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="13" y="194" width="7" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="25" y="194" width="36" height="7" rx="3" fill="#595A6F"/>
|
||||
<rect x="76" y="36" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="76" y="90" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="76" y="144" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="black" fill-opacity="0.4"/>
|
||||
<rect x="76" y="198" width="122" height="44" rx="4" fill="#14141E"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ddd_1994_118694" x="0" y="0" width="206" height="206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1994_118694"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1994_118694" result="effect2_dropShadow_1994_118694"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1994_118694" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_1994_118694">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
70
@xen-orchestra/lite/src/assets/color-mode-light.svg
Normal file
70
@xen-orchestra/lite/src/assets/color-mode-light.svg
Normal file
@@ -0,0 +1,70 @@
|
||||
<svg width="200" height="200" viewBox="3 2 200 200" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ddd_1994_118640)">
|
||||
<g clip-path="url(#clip0_1994_118640)">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="#F6F6F7"/>
|
||||
<rect width="200" height="29" transform="translate(3 2)" fill="#F6F6F7"/>
|
||||
<rect x="13" y="13" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect width="68" height="171" transform="translate(3 31)" fill="white"/>
|
||||
<rect x="13" y="41" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="41" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="58" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="58" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="75" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="75" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="92" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="92" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="109" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="109" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="126" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="126" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="143" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="143" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="160" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="160" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="177" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="177" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="13" y="194" width="7" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="25" y="194" width="36" height="7" rx="3" fill="#E5E5E7"/>
|
||||
<rect x="76" y="36" width="122" height="44" rx="4" fill="white"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="46" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="63" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="76" y="90" width="122" height="44" rx="4" fill="white"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="100" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="117" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="76" y="144" width="122" height="44" rx="4" fill="white"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="154" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="#8F84FF"/>
|
||||
<rect x="86" y="171" width="102" height="7" rx="3" fill="white" fill-opacity="0.6"/>
|
||||
<rect x="76" y="198" width="122" height="44" rx="4" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ddd_1994_118640" x="0" y="0" width="206" height="206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.08 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1994_118640"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.06 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1994_118640" result="effect2_dropShadow_1994_118640"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.101961 0 0 0 0 0.105882 0 0 0 0 0.219608 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_1994_118640" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_1994_118640">
|
||||
<rect x="3" y="2" width="200" height="200" rx="8" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -12,13 +12,23 @@ html {
|
||||
font-family: Poppins, sans-serif;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,85 @@
|
||||
:root {
|
||||
--color-blue-scale-000: #000000;
|
||||
--color-blue-scale-100: #1A1B38;
|
||||
--color-blue-scale-200: #595A6F;
|
||||
--color-blue-scale-300: #9899A5;
|
||||
--color-blue-scale-400: #E5E5E7;
|
||||
--color-blue-scale-500: #FFFFFF;
|
||||
--color-blue-scale-100: #1a1b38;
|
||||
--color-blue-scale-200: #595a6f;
|
||||
--color-blue-scale-300: #9899a5;
|
||||
--color-blue-scale-400: #e5e5e7;
|
||||
--color-blue-scale-500: #ffffff;
|
||||
|
||||
--color-extra-blue-l60: #D1CEFB;
|
||||
--color-extra-blue-l40: #BBB5F9;
|
||||
--color-extra-blue-l20: #A39DF8;
|
||||
--color-extra-blue-base: #8F84FF;
|
||||
--color-extra-blue-d20: #716AC6;
|
||||
--color-extra-blue-d40: #554F94;
|
||||
--color-extra-blue-l60: #d1cefb;
|
||||
--color-extra-blue-l40: #bbb5f9;
|
||||
--color-extra-blue-l20: #a39df8;
|
||||
--color-extra-blue-base: #8f84ff;
|
||||
--color-extra-blue-d20: #716ac6;
|
||||
--color-extra-blue-d40: #554f94;
|
||||
--color-extra-blue-d60: #383563;
|
||||
|
||||
--color-green-infra-l60: #B5DBCA;
|
||||
--color-green-infra-l40: #91C9B0;
|
||||
--color-green-infra-l20: #70B795;
|
||||
--color-green-infra-base: #55A57B;
|
||||
--color-green-infra-l60: #b5dbca;
|
||||
--color-green-infra-l40: #91c9b0;
|
||||
--color-green-infra-l20: #70b795;
|
||||
--color-green-infra-base: #55a57b;
|
||||
--color-green-infra-d20: #438463;
|
||||
--color-green-infra-d40: #32634A;
|
||||
--color-green-infra-d40: #32634a;
|
||||
--color-green-infra-d60: #214231;
|
||||
|
||||
--color-orange-world-l60: #F2CDA8;
|
||||
--color-orange-world-l40: #EBB57D;
|
||||
--color-orange-world-l20: #E59D56;
|
||||
--color-orange-world-base: #EF7F18;
|
||||
--color-orange-world-d20: #BF6612;
|
||||
--color-orange-world-d40: #864F1F;
|
||||
--color-orange-world-d60: #5A3514;
|
||||
--color-orange-world-l60: #f2cda8;
|
||||
--color-orange-world-l40: #ebb57d;
|
||||
--color-orange-world-l20: #e59d56;
|
||||
--color-orange-world-base: #ef7f18;
|
||||
--color-orange-world-d20: #bf6612;
|
||||
--color-orange-world-d40: #864f1f;
|
||||
--color-orange-world-d60: #5a3514;
|
||||
|
||||
--color-red-vates-l60: #DDA5A7;
|
||||
--color-red-vates-l40: #CE787C;
|
||||
--color-red-vates-l20: #BF4F51;
|
||||
--color-red-vates-base: #BE1621;
|
||||
--color-red-vates-d20: #8E2221;
|
||||
--color-red-vates-d40: #6A1919;
|
||||
--color-red-vates-l60: #dda5a7;
|
||||
--color-red-vates-l40: #ce787c;
|
||||
--color-red-vates-l20: #bf4f51;
|
||||
--color-red-vates-base: #be1621;
|
||||
--color-red-vates-d20: #8e2221;
|
||||
--color-red-vates-d40: #6a1919;
|
||||
--color-red-vates-d60: #471010;
|
||||
|
||||
--color-grayscale-200: #585757;
|
||||
|
||||
--background-color-primary: #FFFFFF;
|
||||
--background-color-secondary: #F6F6F7;
|
||||
--background-color-extra-blue: #F4F3FE;
|
||||
--background-color-green-infra: #ECF5F2;
|
||||
--background-color-orange-world: #FBF2E9;
|
||||
--background-color-red-vates: #F5E8E9;
|
||||
--background-color-primary: #ffffff;
|
||||
--background-color-secondary: #f6f6f7;
|
||||
--background-color-extra-blue: #f4f3fe;
|
||||
--background-color-green-infra: #ecf5f2;
|
||||
--background-color-orange-world: #fbf2e9;
|
||||
--background-color-red-vates: #f5e8e9;
|
||||
|
||||
--shadow-100: 0 0.1rem 0.1rem rgba(20, 20, 30, 0.06);
|
||||
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.1), 0 0.2rem 0.1rem rgba(20, 20, 30, 0.06), 0 0.1rem 0.1rem rgba(20, 20, 30, 0.08);
|
||||
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.1), 0 0.1rem 1.8rem rgba(20, 20, 30, 0.06), 0 0.6rem 1.0rem rgba(20, 20, 30, 0.08);
|
||||
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.1), 0 0.9rem 4.6rem rgba(20, 20, 30, 0.06), 0 2.4rem 3.8rem rgba(20, 20, 30, 0.04);
|
||||
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.1),
|
||||
0 0.2rem 0.1rem rgba(20, 20, 30, 0.06),
|
||||
0 0.1rem 0.1rem rgba(20, 20, 30, 0.08);
|
||||
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.1),
|
||||
0 0.1rem 1.8rem rgba(20, 20, 30, 0.06), 0 0.6rem 1rem rgba(20, 20, 30, 0.08);
|
||||
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.1),
|
||||
0 0.9rem 4.6rem rgba(20, 20, 30, 0.06),
|
||||
0 2.4rem 3.8rem rgba(20, 20, 30, 0.04);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--color-blue-scale-000: #FFFFFF;
|
||||
--color-blue-scale-100: #E5E5E7;
|
||||
--color-blue-scale-200: #9899A5;
|
||||
--color-blue-scale-300: #595A6F;
|
||||
--color-blue-scale-400: #1A1B38;
|
||||
--color-blue-scale-000: #ffffff;
|
||||
--color-blue-scale-100: #e5e5e7;
|
||||
--color-blue-scale-200: #9899a5;
|
||||
--color-blue-scale-300: #595a6f;
|
||||
--color-blue-scale-400: #1a1b38;
|
||||
--color-blue-scale-500: #000000;
|
||||
|
||||
--background-color-primary: #14141D;
|
||||
--background-color-secondary: #17182A;
|
||||
--background-color-extra-blue: #35335D;
|
||||
--background-color-green-infra: #243B3D;
|
||||
--background-color-primary: #14141d;
|
||||
--background-color-secondary: #17182a;
|
||||
--background-color-extra-blue: #35335d;
|
||||
--background-color-green-infra: #243b3d;
|
||||
--background-color-orange-world: #493328;
|
||||
--background-color-red-vates: #3C1A28;
|
||||
--background-color-red-vates: #3c1a28;
|
||||
|
||||
--shadow-100: 0 0.1rem 0.1rem rgba(20, 20, 30, 0.12);
|
||||
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.2), 0 0.2rem 0.1rem rgba(20, 20, 30, 0.12), 0 0.1rem 0.1rem rgba(20, 20, 30, 0.16);
|
||||
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.2), 0 0.1rem 1.8rem rgba(20, 20, 30, 0.12), 0 0.6rem 1.0rem rgba(20, 20, 30, 0.16);
|
||||
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.2), 0 0.9rem 4.6rem rgba(20, 20, 30, 0.12), 0 2.4rem 3.8rem rgba(20, 20, 30, 0.08);
|
||||
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.2),
|
||||
0 0.2rem 0.1rem rgba(20, 20, 30, 0.12),
|
||||
0 0.1rem 0.1rem rgba(20, 20, 30, 0.16);
|
||||
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.2),
|
||||
0 0.1rem 1.8rem rgba(20, 20, 30, 0.12), 0 0.6rem 1rem rgba(20, 20, 30, 0.16);
|
||||
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.2),
|
||||
0 0.9rem 4.6rem rgba(20, 20, 30, 0.12),
|
||||
0 2.4rem 3.8rem rgba(20, 20, 30, 0.08);
|
||||
}
|
||||
|
||||
79
@xen-orchestra/lite/src/assets/under-construction.svg
Normal file
79
@xen-orchestra/lite/src/assets/under-construction.svg
Normal file
@@ -0,0 +1,79 @@
|
||||
<svg width="262" height="164" viewBox="0 0 262 164" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_814_56662)">
|
||||
<path d="M249.387 164.001H28.7878C26.826 164.001 24.9446 163.222 23.5575 161.835C22.1703 160.448 21.391 158.567 21.391 156.606C101.358 147.314 179.822 147.314 256.784 156.606C256.784 158.567 256.005 160.448 254.617 161.835C253.23 163.222 251.349 164.001 249.387 164.001Z" fill="#CCCCCC"/>
|
||||
<path d="M256.784 157.04L21.391 156.605L48.6722 110.711L48.8027 110.493V9.34843C48.8026 8.12075 49.0444 6.90507 49.5143 5.77082C49.9841 4.63657 50.6729 3.60597 51.5412 2.73787C52.4095 1.86976 53.4403 1.18117 54.5748 0.7114C55.7093 0.241634 56.9252 -0.000102555 58.1532 3.2638e-08H218.716C219.944 -0.000102555 221.16 0.241634 222.295 0.7114C223.429 1.18117 224.46 1.86976 225.328 2.73787C226.197 3.60597 226.885 4.63657 227.355 5.77082C227.825 6.90507 228.067 8.12075 228.067 9.34843V111.189L256.784 157.04Z" fill="#E6E6E6"/>
|
||||
<path d="M57.94 6.52344C57.1325 6.52434 56.3584 6.84545 55.7874 7.41632C55.2164 7.98719 54.8952 8.7612 54.8943 9.56853V101.791C54.8952 102.599 55.2164 103.373 55.7874 103.944C56.3584 104.514 57.1325 104.835 57.94 104.836H220.235C221.042 104.835 221.817 104.514 222.387 103.943C222.958 103.373 223.28 102.599 223.281 101.791V9.56853C223.28 8.76121 222.959 7.98722 222.388 7.41636C221.817 6.8455 221.042 6.52438 220.235 6.52344H57.94Z" fill="white"/>
|
||||
<path d="M59.0067 117.02C58.7572 117.02 58.5129 117.092 58.3028 117.226C58.0927 117.361 57.9255 117.553 57.8208 117.779L49.4146 136.05C49.3231 136.248 49.283 136.467 49.2982 136.685C49.3133 136.904 49.3831 137.115 49.5012 137.299C49.6193 137.483 49.7819 137.635 49.9739 137.74C50.166 137.845 50.3814 137.9 50.6003 137.9H227.378C227.6 137.9 227.819 137.843 228.014 137.735C228.208 137.627 228.371 137.47 228.488 137.281C228.605 137.092 228.672 136.876 228.682 136.654C228.692 136.431 228.645 136.21 228.545 136.011L219.408 117.741C219.3 117.524 219.134 117.341 218.927 117.214C218.721 117.086 218.483 117.019 218.241 117.02L59.0067 117.02Z" fill="#CCCCCC"/>
|
||||
<path d="M138.435 5.00106C139.516 5.00106 140.393 4.12463 140.393 3.0435C140.393 1.96237 139.516 1.08594 138.435 1.08594C137.353 1.08594 136.477 1.96237 136.477 3.0435C136.477 4.12463 137.353 5.00106 138.435 5.00106Z" fill="white"/>
|
||||
<path d="M121.059 141.379C120.772 141.379 120.493 141.474 120.265 141.649C120.038 141.823 119.874 142.068 119.799 142.345L117.69 150.175C117.638 150.368 117.631 150.571 117.67 150.768C117.709 150.964 117.792 151.149 117.914 151.308C118.036 151.467 118.193 151.596 118.373 151.684C118.552 151.773 118.75 151.819 118.95 151.819H159.051C159.259 151.819 159.465 151.769 159.649 151.674C159.834 151.579 159.994 151.441 160.115 151.271C160.235 151.102 160.314 150.906 160.343 150.7C160.373 150.494 160.353 150.284 160.284 150.087L157.574 142.257C157.485 142.001 157.318 141.778 157.097 141.621C156.876 141.464 156.611 141.379 156.34 141.379H121.059Z" fill="#CCCCCC"/>
|
||||
<path d="M228.067 108.969V110.709H48.6722L48.8071 110.491V108.969H228.067Z" fill="#CCCCCC"/>
|
||||
<path d="M205.924 112.957C205.854 112.957 205.786 112.98 205.731 113.023C205.675 113.065 205.635 113.125 205.617 113.192L205.104 115.099C205.091 115.146 205.089 115.195 205.099 115.243C205.108 115.291 205.129 115.336 205.158 115.375C205.188 115.414 205.226 115.445 205.27 115.467C205.314 115.488 205.362 115.499 205.411 115.499H215.176C215.226 115.499 215.276 115.487 215.321 115.464C215.366 115.441 215.405 115.407 215.435 115.366C215.464 115.325 215.483 115.277 215.49 115.227C215.498 115.177 215.493 115.126 215.476 115.078L214.816 113.171C214.794 113.108 214.754 113.054 214.7 113.016C214.646 112.978 214.582 112.957 214.516 112.957H205.924Z" fill="#CCCCCC"/>
|
||||
<path d="M191.666 112.957C191.596 112.957 191.528 112.98 191.472 113.023C191.417 113.065 191.377 113.125 191.359 113.192L190.845 115.099C190.833 115.146 190.831 115.195 190.84 115.243C190.85 115.291 190.87 115.336 190.9 115.375C190.93 115.414 190.968 115.445 191.012 115.467C191.055 115.488 191.103 115.499 191.152 115.499H200.917C200.968 115.499 201.018 115.487 201.063 115.464C201.108 115.441 201.147 115.407 201.176 115.366C201.206 115.325 201.225 115.277 201.232 115.227C201.239 115.177 201.234 115.126 201.218 115.078L200.557 113.171C200.536 113.108 200.495 113.054 200.441 113.016C200.388 112.978 200.323 112.957 200.257 112.957H191.666Z" fill="#CCCCCC"/>
|
||||
<path d="M150.356 125.071H151.121L149.591 22.793H148.825L148.856 24.8334H131.702L131.733 22.793H130.967L129.437 125.071H130.202L130.301 118.44H150.257L150.356 125.071ZM149.253 51.3595H131.305L131.492 38.8617H149.066L149.253 51.3595ZM149.264 52.1247L149.451 64.6226H131.107L131.294 52.1247H149.264ZM149.463 65.3877L149.65 77.8856H130.908L131.095 65.3877H149.463ZM149.661 78.6508L149.848 91.1486H130.71L130.897 78.6508H149.661ZM149.86 91.9138L150.047 104.412H130.511L130.698 91.9138H149.86ZM148.867 25.5986L149.054 38.0965H131.504L131.691 25.5986H148.867ZM130.313 117.675L130.5 105.177H150.058L150.245 117.675H130.313Z" fill="#3F3D56"/>
|
||||
<path d="M136.488 58.0205C143.38 58.0205 148.967 52.4344 148.967 45.5435C148.967 38.6526 143.38 33.0664 136.488 33.0664C129.595 33.0664 124.008 38.6526 124.008 45.5435C124.008 52.4344 129.595 58.0205 136.488 58.0205Z" fill="#2F2E41"/>
|
||||
<path d="M143.828 54.1685L140.098 54.8438L141.308 61.5265L145.038 60.8512L143.828 54.1685Z" fill="#2F2E41"/>
|
||||
<path d="M136.367 55.5201L132.636 56.1953L133.846 62.878L137.577 62.2028L136.367 55.5201Z" fill="#2F2E41"/>
|
||||
<path d="M135.827 64.2821C137.158 63.1546 137.895 61.836 137.472 61.3367C137.049 60.8375 135.627 61.3468 134.296 62.4742C132.964 63.6017 132.227 64.9203 132.65 65.4196C133.073 65.9188 134.495 65.4095 135.827 64.2821Z" fill="#2F2E41"/>
|
||||
<path d="M143.133 62.9618C144.464 61.8343 145.201 60.5156 144.778 60.0164C144.355 59.5172 142.933 60.0264 141.601 61.1539C140.27 62.2813 139.533 63.6 139.956 64.0992C140.379 64.5985 141.801 64.0892 143.133 62.9618Z" fill="#2F2E41"/>
|
||||
<path d="M135.614 46.7591C137.97 46.7591 139.879 44.8499 139.879 42.4948C139.879 40.1397 137.97 38.2305 135.614 38.2305C133.258 38.2305 131.349 40.1397 131.349 42.4948C131.349 44.8499 133.258 46.7591 135.614 46.7591Z" fill="white"/>
|
||||
<path d="M133.861 42.3858C134.646 42.3858 135.283 41.7494 135.283 40.9644C135.283 40.1794 134.646 39.543 133.861 39.543C133.076 39.543 132.439 40.1794 132.439 40.9644C132.439 41.7494 133.076 42.3858 133.861 42.3858Z" fill="#3F3D56"/>
|
||||
<path d="M146.333 31.97C146.52 27.3508 142.538 23.4383 137.439 23.2312C132.339 23.0241 128.053 26.6008 127.865 31.22C127.677 35.8392 131.222 36.8887 136.322 37.0958C141.422 37.303 146.145 36.5892 146.333 31.97Z" fill="#8F84FF"/>
|
||||
<path d="M131.047 44.6638C131.288 43.6096 128.761 42.1308 125.402 41.3611C122.043 40.5913 119.124 40.8219 118.882 41.8762C118.641 42.9305 121.168 44.4092 124.527 45.179C127.886 45.9488 130.805 45.7181 131.047 44.6638Z" fill="#2F2E41"/>
|
||||
<path d="M155.544 48.7381C155.786 47.6838 153.258 46.2051 149.899 45.4353C146.54 44.6655 143.621 44.8961 143.38 45.9504C143.138 47.0047 145.665 48.4834 149.024 49.2532C152.383 50.023 155.302 49.7924 155.544 48.7381Z" fill="#2F2E41"/>
|
||||
<path d="M139.915 50.4628C139.983 50.8302 139.977 51.2073 139.898 51.5724C139.82 51.9376 139.67 52.2837 139.457 52.591C139.245 52.8982 138.974 53.1606 138.66 53.3631C138.346 53.5656 137.995 53.7043 137.628 53.7712C137.26 53.8381 136.883 53.8319 136.518 53.753C136.152 53.6741 135.806 53.524 135.499 53.3113C135.192 53.0986 134.93 52.8275 134.728 52.5135C134.525 52.1994 134.387 51.8486 134.32 51.4811L134.319 51.4756C134.04 49.9306 135.234 49.3839 136.78 49.1042C138.325 48.8245 139.636 48.9178 139.915 50.4628Z" fill="white"/>
|
||||
<path d="M94.2194 87.9208C95.4807 84.6216 95.6634 81.6262 94.6275 81.2303C93.5915 80.8344 91.7293 83.188 90.468 86.4872C89.2067 89.7864 89.024 92.7819 90.0599 93.1777C91.0959 93.5736 92.9581 91.22 94.2194 87.9208Z" fill="#2F2E41"/>
|
||||
<path d="M93.147 98.9138C94.2787 91.941 89.5425 85.3712 82.5682 84.2397C75.594 83.1082 69.0228 87.8434 67.891 94.8162C66.7593 101.789 71.4955 108.359 78.4698 109.49C85.444 110.622 92.0152 105.886 93.147 98.9138Z" fill="#2F2E41"/>
|
||||
<path d="M78.5757 106.902H74.6892V113.864H78.5757V106.902Z" fill="#2F2E41"/>
|
||||
<path d="M86.3487 106.902H82.4622V113.864H86.3487V106.902Z" fill="#2F2E41"/>
|
||||
<path d="M77.928 115.159C79.7167 115.159 81.1668 114.615 81.1668 113.945C81.1668 113.274 79.7167 112.73 77.928 112.73C76.1393 112.73 74.6892 113.274 74.6892 113.945C74.6892 114.615 76.1393 115.159 77.928 115.159Z" fill="#2F2E41"/>
|
||||
<path d="M85.7009 114.995C87.4897 114.995 88.9397 114.451 88.9397 113.781C88.9397 113.11 87.4897 112.566 85.7009 112.566C83.9122 112.566 82.4622 113.11 82.4622 113.781C82.4622 114.451 83.9122 114.995 85.7009 114.995Z" fill="#2F2E41"/>
|
||||
<path d="M68.1092 84.9687C67.0762 80.3435 70.3782 75.6699 75.4845 74.53C80.5908 73.39 85.5676 76.2154 86.6006 80.8406C87.6336 85.4658 84.2495 87.1716 79.1432 88.3116C74.037 89.4515 69.1422 89.5939 68.1092 84.9687Z" fill="#8F84FF"/>
|
||||
<path d="M71.8891 93.671C72.3643 92.6692 70.162 90.6301 66.9701 89.1165C63.7782 87.603 60.8054 87.1881 60.3301 88.19C59.8549 89.1918 62.0572 91.2309 65.2491 92.7444C68.441 94.258 71.4138 94.6728 71.8891 93.671Z" fill="#2F2E41"/>
|
||||
<path d="M25.1257 122.604C32.1911 122.604 37.9188 116.878 37.9188 109.814C37.9188 102.75 32.1911 97.0234 25.1257 97.0234C18.0602 97.0234 12.3325 102.75 12.3325 109.814C12.3325 116.878 18.0602 122.604 25.1257 122.604Z" fill="#2F2E41"/>
|
||||
<path d="M23.1824 119.852H19.2959V126.813H23.1824V119.852Z" fill="#2F2E41"/>
|
||||
<path d="M30.9554 119.852H27.0688V126.813H30.9554V119.852Z" fill="#2F2E41"/>
|
||||
<path d="M22.5347 128.108C24.3234 128.108 25.7735 127.565 25.7735 126.894C25.7735 126.223 24.3234 125.68 22.5347 125.68C20.7459 125.68 19.2959 126.223 19.2959 126.894C19.2959 127.565 20.7459 128.108 22.5347 128.108Z" fill="#2F2E41"/>
|
||||
<path d="M30.3076 127.948C32.0963 127.948 33.5464 127.404 33.5464 126.734C33.5464 126.063 32.0963 125.52 30.3076 125.52C28.5189 125.52 27.0688 126.063 27.0688 126.734C27.0688 127.404 28.5189 127.948 30.3076 127.948Z" fill="#2F2E41"/>
|
||||
<path d="M25.4495 110.946C27.8643 110.946 29.8218 108.989 29.8218 106.575C29.8218 104.16 27.8643 102.203 25.4495 102.203C23.0347 102.203 21.0771 104.16 21.0771 106.575C21.0771 108.989 23.0347 110.946 25.4495 110.946Z" fill="white"/>
|
||||
<path d="M25.4496 108.035C26.2546 108.035 26.9071 107.383 26.9071 106.578C26.9071 105.773 26.2546 105.121 25.4496 105.121C24.6447 105.121 23.9922 105.773 23.9922 106.578C23.9922 107.383 24.6447 108.035 25.4496 108.035Z" fill="#3F3D56"/>
|
||||
<path d="M12.7159 97.9179C11.6829 93.2927 14.9849 88.6191 20.0912 87.4792C25.1975 86.3392 30.1743 89.1646 31.2073 93.7898C32.2403 98.415 28.8562 100.121 23.7499 101.261C18.6436 102.401 13.7489 102.543 12.7159 97.9179Z" fill="#E6E6E6"/>
|
||||
<path d="M34.4538 64.12C35.7151 60.8208 35.8978 57.8254 34.8618 57.4295C33.8259 57.0336 31.9636 59.3872 30.7023 62.6864C29.4411 65.9856 29.2584 68.9811 30.2943 69.3769C31.3302 69.7728 33.1925 67.4192 34.4538 64.12Z" fill="#2F2E41"/>
|
||||
<path d="M33.3813 75.113C34.5131 68.1402 29.7768 61.5704 22.8026 60.4389C15.8284 59.3074 9.25717 64.0427 8.12541 71.0154C6.99365 77.9881 11.7299 84.5579 18.7041 85.6894C25.6784 86.8209 32.2496 82.0857 33.3813 75.113Z" fill="#2F2E41"/>
|
||||
<path d="M18.81 83.1016H14.9235V90.0634H18.81V83.1016Z" fill="#2F2E41"/>
|
||||
<path d="M26.5831 83.1016H22.6965V90.0634H26.5831V83.1016Z" fill="#2F2E41"/>
|
||||
<path d="M18.1622 91.3583C19.951 91.3583 21.401 90.8146 21.401 90.144C21.401 89.4733 19.951 88.9297 18.1622 88.9297C16.3735 88.9297 14.9235 89.4733 14.9235 90.144C14.9235 90.8146 16.3735 91.3583 18.1622 91.3583Z" fill="#2F2E41"/>
|
||||
<path d="M25.9353 91.1942C27.724 91.1942 29.1741 90.6505 29.1741 89.9799C29.1741 89.3093 27.724 88.7656 25.9353 88.7656C24.1466 88.7656 22.6965 89.3093 22.6965 89.9799C22.6965 90.6505 24.1466 91.1942 25.9353 91.1942Z" fill="#2F2E41"/>
|
||||
<path d="M21.0772 74.1959C23.492 74.1959 25.4495 72.2388 25.4495 69.8245C25.4495 67.4103 23.492 65.4531 21.0772 65.4531C18.6624 65.4531 16.7048 67.4103 16.7048 69.8245C16.7048 72.2388 18.6624 74.1959 21.0772 74.1959Z" fill="white"/>
|
||||
<path d="M21.0772 71.2815C21.8821 71.2815 22.5347 70.6291 22.5347 69.8243C22.5347 69.0196 21.8821 68.3672 21.0772 68.3672C20.2723 68.3672 19.6198 69.0196 19.6198 69.8243C19.6198 70.6291 20.2723 71.2815 21.0772 71.2815Z" fill="#3F3D56"/>
|
||||
<path d="M8.34355 61.1679C7.31057 56.5427 10.6126 51.8691 15.7189 50.7292C20.8252 49.5892 25.802 52.4146 26.835 57.0398C27.868 61.665 24.4839 63.3709 19.3776 64.5108C14.2713 65.6507 9.37653 65.7931 8.34355 61.1679Z" fill="#8F84FF"/>
|
||||
<path d="M12.1235 69.8663C12.5987 68.8645 10.3964 66.8254 7.20451 65.3118C4.01259 63.7983 1.03975 63.3834 0.5645 64.3853C0.0892476 65.3871 2.29155 67.4262 5.48347 68.9398C8.67539 70.4533 11.6482 70.8681 12.1235 69.8663Z" fill="#2F2E41"/>
|
||||
<path d="M14.4694 77.6771C14.4694 78.9289 17.6933 81.4009 21.2708 81.4009C24.8482 81.4009 28.2025 77.8765 28.2025 76.6247C28.2025 75.3729 24.8482 76.8676 21.2708 76.8676C17.6933 76.8676 14.4694 76.4252 14.4694 77.6771Z" fill="white"/>
|
||||
<path d="M33.3311 24.2266C33.0948 24.2268 32.8683 24.3208 32.7012 24.4878C32.5342 24.6548 32.4402 24.8813 32.4399 25.1175V61.9438C32.4402 62.18 32.5342 62.4065 32.7012 62.5735C32.8683 62.7405 33.0948 62.8345 33.3311 62.8347H118.584C118.821 62.8345 119.047 62.7405 119.214 62.5735C119.381 62.4065 119.475 62.18 119.476 61.9438V25.1175C119.475 24.8813 119.381 24.6548 119.214 24.4878C119.047 24.3208 118.821 24.2268 118.584 24.2266H33.3311Z" fill="#8F84FF"/>
|
||||
<path d="M56.3507 52.176C61.1282 52.176 65.0012 48.3039 65.0012 43.5275C65.0012 38.751 61.1282 34.8789 56.3507 34.8789C51.5732 34.8789 47.7003 38.751 47.7003 43.5275C47.7003 48.3039 51.5732 52.176 56.3507 52.176Z" fill="white"/>
|
||||
<path d="M75.8746 42.379C75.6092 42.3834 75.3562 42.4919 75.1701 42.681C74.9839 42.8702 74.8796 43.1249 74.8796 43.3902C74.8796 43.6556 74.9839 43.9103 75.1701 44.0994C75.3562 44.2886 75.6092 44.3971 75.8746 44.4014H103.187C103.32 44.4037 103.452 44.3798 103.576 44.331C103.699 44.2823 103.812 44.2096 103.908 44.1173C104.003 44.0249 104.08 43.9146 104.132 43.7927C104.185 43.6708 104.214 43.5397 104.216 43.4068C104.218 43.274 104.194 43.142 104.145 43.0184C104.096 42.8949 104.024 42.7821 103.931 42.6867C103.839 42.5912 103.728 42.5149 103.606 42.4621C103.484 42.4094 103.353 42.3811 103.22 42.379C103.209 42.3789 103.198 42.3789 103.187 42.379H75.8746Z" fill="white"/>
|
||||
<path d="M75.8746 36.5197C75.6092 36.524 75.3562 36.6325 75.1701 36.8217C74.9839 37.0108 74.8796 37.2655 74.8796 37.5309C74.8796 37.7962 74.9839 38.0509 75.1701 38.2401C75.3562 38.4292 75.6092 38.5377 75.8746 38.5421H89.5138C89.6467 38.5443 89.7787 38.5204 89.9023 38.4717C90.026 38.4229 90.1388 38.3503 90.2343 38.2579C90.3299 38.1656 90.4063 38.0553 90.4592 37.9334C90.5121 37.8115 90.5404 37.6803 90.5426 37.5475C90.5448 37.4146 90.5208 37.2826 90.4719 37.1591C90.423 37.0355 90.3503 36.9228 90.2578 36.8273C90.1654 36.7318 90.055 36.6555 89.933 36.6028C89.8111 36.55 89.6799 36.5217 89.547 36.5197C89.5359 36.5195 89.5248 36.5195 89.5138 36.5197H75.8746Z" fill="white"/>
|
||||
<path d="M75.8746 48.5158C75.6092 48.5201 75.3562 48.6286 75.1701 48.8177C74.9839 49.0069 74.8796 49.2616 74.8796 49.527C74.8796 49.7923 74.9839 50.047 75.1701 50.2362C75.3562 50.4253 75.6092 50.5338 75.8746 50.5382H103.187C103.32 50.5404 103.452 50.5165 103.576 50.4678C103.699 50.419 103.812 50.3464 103.908 50.254C104.003 50.1617 104.08 50.0514 104.132 49.9295C104.185 49.8076 104.214 49.6764 104.216 49.5436C104.218 49.4107 104.194 49.2787 104.145 49.1552C104.096 49.0316 104.024 48.9189 103.931 48.8234C103.839 48.728 103.728 48.6517 103.606 48.5989C103.484 48.5461 103.353 48.5178 103.22 48.5158C103.209 48.5156 103.198 48.5156 103.187 48.5158H75.8746Z" fill="white"/>
|
||||
<path d="M208.521 116.633C209.557 116.237 209.375 113.242 208.113 109.942C206.852 106.643 204.99 104.29 203.954 104.686C202.918 105.081 203.1 108.077 204.362 111.376C205.623 114.675 207.485 117.029 208.521 116.633Z" fill="#2F2E41"/>
|
||||
<path d="M217.438 135.616C224.412 134.484 229.149 127.914 228.017 120.942C226.885 113.969 220.314 109.234 213.34 110.365C206.365 111.497 201.629 118.067 202.761 125.039C203.893 132.012 210.464 136.747 217.438 135.616Z" fill="#2F2E41"/>
|
||||
<path d="M221.218 133.031H217.332L218.093 141.369L221.225 140.518L221.218 133.031Z" fill="#2F2E41"/>
|
||||
<path d="M212.746 133.051H208.859L209.62 141.388L212.611 140.516L212.746 133.051Z" fill="#2F2E41"/>
|
||||
<path d="M209.509 141.78C211.293 141.649 212.7 141.001 212.651 140.333C212.602 139.664 211.116 139.227 209.332 139.358C207.548 139.488 206.141 140.136 206.19 140.805C206.239 141.474 207.725 141.91 209.509 141.78Z" fill="#2F2E41"/>
|
||||
<path d="M215.065 124.126C217.48 124.126 219.437 122.168 219.437 119.754C219.437 117.34 217.48 115.383 215.065 115.383C212.65 115.383 210.693 117.34 210.693 119.754C210.693 122.168 212.65 124.126 215.065 124.126Z" fill="white"/>
|
||||
<path d="M215.065 121.211C215.87 121.211 216.522 120.559 216.522 119.754C216.522 118.949 215.87 118.297 215.065 118.297C214.26 118.297 213.607 118.949 213.607 119.754C213.607 120.559 214.26 121.211 215.065 121.211Z" fill="#3F3D56"/>
|
||||
<path d="M230.522 115.281C232.621 112.439 233.599 109.602 232.707 108.944C231.815 108.285 229.391 110.054 227.292 112.895C225.193 115.736 224.215 118.573 225.107 119.232C225.999 119.891 228.423 118.122 230.522 115.281Z" fill="#2F2E41"/>
|
||||
<path d="M209.147 129.585C209.147 128.553 211.804 126.516 214.753 126.516C217.701 126.516 220.466 129.42 220.466 130.452C220.466 131.484 217.701 130.252 214.753 130.252C211.804 130.252 209.147 130.616 209.147 129.585Z" fill="white"/>
|
||||
<path d="M251.263 112.42L181.234 102.714C180.688 102.638 180.195 102.348 179.863 101.908C179.53 101.469 179.385 100.915 179.46 100.369L187.333 43.5939C187.409 43.0481 187.699 42.5548 188.138 42.2222C188.578 41.8897 189.132 41.7451 189.678 41.8202L259.706 51.526C260.252 51.6023 260.745 51.892 261.078 52.3316C261.411 52.7711 261.555 53.3246 261.48 53.8707L253.608 110.646C253.531 111.192 253.242 111.685 252.802 112.018C252.362 112.35 251.809 112.495 251.263 112.42Z" fill="#8F84FF"/>
|
||||
<path d="M226.175 94.7045L190.424 89.7494C190.379 89.7438 190.335 89.734 190.292 89.7203L211.261 62.7818C211.389 62.6137 211.56 62.4828 211.756 62.4023C211.952 62.3219 212.165 62.2948 212.375 62.3238C212.584 62.3529 212.782 62.437 212.949 62.5677C213.115 62.6983 213.244 62.8708 213.322 63.0674L221.945 84.2907L222.358 85.3064L226.175 94.7045Z" fill="white"/>
|
||||
<path opacity="0.2" d="M226.175 94.7026L213.969 93.011L221.168 85.1396L221.686 84.5727L221.945 84.2891L222.358 85.3047L226.175 94.7026Z" fill="black"/>
|
||||
<path d="M246.081 97.4603L215.16 93.1745L222.358 85.3032L222.876 84.7361L232.257 74.4782C232.419 74.3241 232.612 74.2058 232.822 74.131C233.033 74.0561 233.257 74.0265 233.48 74.0439C233.703 74.0613 233.92 74.1255 234.117 74.2321C234.313 74.3388 234.485 74.4856 234.622 74.663C234.656 74.7129 234.687 74.765 234.715 74.8188L246.081 97.4603Z" fill="white"/>
|
||||
<path d="M226.61 71.9259C229.563 71.9259 231.957 69.5325 231.957 66.5801C231.957 63.6277 229.563 61.2344 226.61 61.2344C223.657 61.2344 221.263 63.6277 221.263 66.5801C221.263 69.5325 223.657 71.9259 226.61 71.9259Z" fill="white"/>
|
||||
<path d="M197.346 47.0847L190.872 46.1875L189.975 52.6593L196.448 53.5565L197.346 47.0847Z" fill="#3F3D56"/>
|
||||
<path d="M227.799 111.098C228.832 106.472 225.529 101.799 220.423 100.659C215.317 99.5189 210.34 102.344 209.307 106.969C208.274 111.595 211.658 113.301 216.764 114.44C221.871 115.58 226.766 115.723 227.799 111.098Z" fill="#F2F2F2"/>
|
||||
<path d="M218.032 141.76C219.816 141.63 221.223 140.982 221.174 140.313C221.125 139.644 219.639 139.208 217.855 139.338C216.071 139.469 214.665 140.116 214.714 140.785C214.762 141.454 216.248 141.891 218.032 141.76Z" fill="#2F2E41"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_814_56662">
|
||||
<rect width="261" height="164" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
1
@xen-orchestra/lite/src/assets/undraw-bug-fixing.svg
Normal file
1
@xen-orchestra/lite/src/assets/undraw-bug-fixing.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 40 KiB |
@@ -34,7 +34,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import AppMenu from "@/components/menu/AppMenu.vue";
|
||||
import MenuItem from "@/components/menu/MenuItem.vue";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useNavigationStore } from "@/stores/navigation.store";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { faBars } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
115
@xen-orchestra/lite/src/components/AppMarkdown.vue
Normal file
115
@xen-orchestra/lite/src/components/AppMarkdown.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div ref="rootElement" class="app-markdown" v-html="html" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import markdown from "@/libs/markdown";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import { computed, type Ref, ref } from "vue";
|
||||
|
||||
const rootElement = ref() as Ref<HTMLElement>;
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
}>();
|
||||
|
||||
const html = computed(() => markdown.parse(props.content ?? ""));
|
||||
|
||||
useEventListener(
|
||||
rootElement,
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (!target.classList.contains("copy-button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const copyable =
|
||||
target.parentElement!.querySelector<HTMLElement>(".copyable");
|
||||
|
||||
if (copyable !== null) {
|
||||
navigator.clipboard.writeText(copyable.innerText);
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.app-markdown {
|
||||
font-size: 1.6rem;
|
||||
|
||||
:deep() {
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1.4rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
code:not(.hljs-code) {
|
||||
background-color: var(--background-color-extra-blue);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: revert;
|
||||
padding-left: 2rem;
|
||||
list-style-type: revert;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
thead th {
|
||||
border-bottom: 2px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 1rem;
|
||||
top: 0.4rem;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-d20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,19 @@
|
||||
<template>
|
||||
<nav
|
||||
v-if="isDesktop || isOpen"
|
||||
ref="navElement"
|
||||
:class="{ collapsible: isMobile }"
|
||||
class="app-navigation"
|
||||
>
|
||||
<InfraPoolList />
|
||||
</nav>
|
||||
<transition name="slide">
|
||||
<nav
|
||||
v-if="isDesktop || isOpen"
|
||||
ref="navElement"
|
||||
:class="{ collapsible: isMobile }"
|
||||
class="app-navigation"
|
||||
>
|
||||
<StoryMenu v-if="$route.meta.hasStoryNav" />
|
||||
<InfraPoolList v-else />
|
||||
</nav>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import StoryMenu from "@/components/component-story/StoryMenu.vue";
|
||||
import InfraPoolList from "@/components/infra/InfraPoolList.vue";
|
||||
import { useNavigationStore } from "@/stores/navigation.store";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
@@ -44,7 +48,7 @@ whenever(isOpen, () => {
|
||||
overflow: auto;
|
||||
width: 37rem;
|
||||
max-width: 37rem;
|
||||
height: calc(100vh - 9rem);
|
||||
height: calc(100vh - 8rem);
|
||||
padding: 0.5rem;
|
||||
border-right: 1px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-primary);
|
||||
@@ -54,4 +58,13 @@ whenever(isOpen, () => {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
transform: translateX(-37rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div v-if="!isDisabled" ref="tooltipElement" class="app-tooltip">
|
||||
<span class="triangle" />
|
||||
<span class="label">{{ content }}</span>
|
||||
<span class="label">{{ options.content }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isEmpty, isFunction, isString } from "lodash-es";
|
||||
import type { TooltipOptions } from "@/stores/tooltip.store";
|
||||
import { isString } from "lodash-es";
|
||||
import place from "placement.js";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import type { TooltipOptions } from "@/stores/tooltip.store";
|
||||
|
||||
const props = defineProps<{
|
||||
target: HTMLElement;
|
||||
@@ -18,29 +18,13 @@ const props = defineProps<{
|
||||
|
||||
const tooltipElement = ref<HTMLElement>();
|
||||
|
||||
const content = computed(() =>
|
||||
isString(props.options) ? props.options : props.options.content
|
||||
const isDisabled = computed(() =>
|
||||
isString(props.options.content)
|
||||
? props.options.content.trim() === ""
|
||||
: props.options.content === false
|
||||
);
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
if (isEmpty(content.value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isString(props.options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFunction(props.options.disabled)) {
|
||||
return props.options.disabled(props.target);
|
||||
}
|
||||
|
||||
return props.options.disabled ?? false;
|
||||
});
|
||||
|
||||
const placement = computed(() =>
|
||||
isString(props.options) ? "top" : props.options.placement ?? "top"
|
||||
);
|
||||
const placement = computed(() => props.options.placement ?? "top");
|
||||
|
||||
watchEffect(() => {
|
||||
if (tooltipElement.value) {
|
||||
@@ -62,6 +46,7 @@ watchEffect(() => {
|
||||
color: var(--color-blue-scale-500);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--color-blue-scale-100);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.triangle {
|
||||
|
||||
41
@xen-orchestra/lite/src/components/CodeHighlight.vue
Normal file
41
@xen-orchestra/lite/src/components/CodeHighlight.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<pre class="code-highlight hljs"><code v-html="codeAsHtml"></code></pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HLJS from "highlight.js";
|
||||
import { computed } from "vue";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
code?: any;
|
||||
lang?: string;
|
||||
}>(),
|
||||
{ lang: "typescript" }
|
||||
);
|
||||
|
||||
const codeAsText = computed(() => {
|
||||
switch (typeof props.code) {
|
||||
case "string":
|
||||
return props.code;
|
||||
case "function":
|
||||
return String(props.code);
|
||||
default:
|
||||
return JSON.stringify(props.code, undefined, 2);
|
||||
}
|
||||
});
|
||||
|
||||
const codeAsHtml = computed(
|
||||
() => HLJS.highlight(codeAsText.value, { language: props.lang }).value
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.code-highlight {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.6rem;
|
||||
text-align: left;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user