Compare commits

...

128 Commits

Author SHA1 Message Date
florent Beauchamp
a15428ac88 fix(@xen-orchestra/vmware-explorer): cleanup 2023-02-07 10:54:28 +01:00
ggunullu
85a23c68f2 remove ignore-pattern for vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
c16c1f8eb9 remove checkFile from util.js 2023-02-07 10:42:56 +01:00
ggunullu
8af95b41fd test 2023-02-07 10:42:56 +01:00
ggunullu
d0e3603663 upgrade node version in package 2023-02-07 10:42:56 +01:00
ggunullu
2e755ec083 test 2023-02-07 10:42:56 +01:00
ggunullu
724195d66d use unlink and move test file 2023-02-07 10:42:56 +01:00
ggunullu
b132ff4fd0 remove unused test 2023-02-07 10:42:56 +01:00
ggunullu
6f1054e2d1 remove ignore-pattern on vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
60c59a0529 test 2023-02-07 10:42:56 +01:00
ggunullu
d382f262fd change file to remove 2023-02-07 10:42:56 +01:00
ggunullu
f6baef3bd6 test 2023-02-07 10:42:56 +01:00
ggunullu
4a27fd35bf remove ignore-pattern on vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
edd37be295 test 2023-02-07 10:42:56 +01:00
ggunullu
e38f00c18b test 2023-02-07 10:42:56 +01:00
ggunullu
24b08037f9 test 2023-02-07 10:42:56 +01:00
ggunullu
1d9bc390bb test 2023-02-07 10:42:56 +01:00
ggunullu
44ba19990e test 2023-02-07 10:42:56 +01:00
ggunullu
5571a1c262 test 2023-02-07 10:42:56 +01:00
ggunullu
9617241b6d test 2023-02-07 10:42:56 +01:00
ggunullu
4b5eadcf88 test 2023-02-07 10:42:56 +01:00
ggunullu
c76295e5c9 test 2023-02-07 10:42:56 +01:00
ggunullu
b61ab4c79a test 2023-02-07 10:42:56 +01:00
ggunullu
2d01192204 Test 2023-02-07 10:42:56 +01:00
ggunullu
eb6763b0bb test 2023-02-07 10:42:56 +01:00
ggunullu
2bb935e9ca test 2023-02-07 10:42:56 +01:00
ggunullu
1e72e9d749 test 2023-02-07 10:42:56 +01:00
ggunullu
59700834cc test 2023-02-07 10:42:56 +01:00
ggunullu
95d6ed0376 test 2023-02-07 10:42:56 +01:00
ggunullu
5dfc8b2e0a test 2023-02-07 10:42:56 +01:00
ggunullu
6961361cf8 test 2023-02-07 10:42:56 +01:00
ggunullu
c105057b91 test 2023-02-07 10:42:56 +01:00
ggunullu
29b20753e9 test 2023-02-07 10:42:56 +01:00
ggunullu
f0b93dc7fe test 2023-02-07 10:42:56 +01:00
ggunullu
dd2b054b35 set back vmware-explorer test 2023-02-07 10:42:56 +01:00
ggunullu
bc09387f5e ignore vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
6e8e725a94 chore(test): remove vhd-util check 2023-02-07 10:42:56 +01:00
Julien Fontanet
55920a58a3 feat(xo-server/recover-account): -s flag for xoa-support
Simpler process for xoa-support.

```console
$ xo-server-recover-account -s
The generated password is lXJMtCzWDGPOIg
user xoa-support has been successfully updated
```
2023-02-06 15:25:04 +01:00
Julien Fontanet
2a70ebf667 docs: uniformize code blocks
- add missing syntaxes
- don't put prompt if no command outputs to ease copy/paste and use `sh` syntax
- always use `$` as prompt and use `console` syntax
2023-02-06 11:25:12 +01:00
Julien Fontanet
2f65a86aa0 fix(xen-api/putResource): fix a number of issues
- hide `VDI_IO_ERROR` when using content-length hack
- avoid unhandled rejection in case upload fails
2023-02-06 10:40:42 +01:00
Julien Fontanet
4bf81ac33b docs(xapi): fix typo 2023-02-04 11:14:02 +01:00
Julien Fontanet
263c23ae8f docs(xapi): describe syncHookTimeout 2023-02-04 11:11:41 +01:00
Julien Fontanet
bf51b945c5 chore(vmware-explorer): fix lint issues
Introduced by 9fa15d9c8
2023-02-03 16:36:55 +01:00
Julien Fontanet
9d7a461550 feat(turbo): add dev and test tasks 2023-02-03 16:17:52 +01:00
Julien Fontanet
bbf60818eb chore: update dev deps 2023-02-03 16:17:31 +01:00
Julien Fontanet
103b22ebb2 fix(backups/importDeltaVm): resize cloned VDI if necessary
Fixes zammad#10996
2023-02-03 15:49:08 +01:00
Mathieu
cf4a1d7d40 fix(lite): update stacked ram usage message (#6650) 2023-02-02 11:50:10 +01:00
Julien Fontanet
e94f036aca chore(vmware-explorer): lower requirement to Node 14 2023-02-02 09:43:03 +01:00
Julien Fontanet
675405f7ac feat: release 5.79.0 2023-01-31 17:49:51 +01:00
Thierry Goettelmann
f8a3536a88 feat(lite): RelativeTime component (#6620) 2023-01-31 17:10:26 +01:00
Julien Fontanet
e527a13b50 feat(xo-server): 5.109.0 2023-01-31 17:04:31 +01:00
Julien Fontanet
3be03451f8 feat(@xen-orchestra/vmware-explorer): 0.0.3 2023-01-31 17:02:24 +01:00
Florent BEAUCHAMP
9fa15d9c84 feat(xo-server): import VM from ESXi (#6595) 2023-01-31 16:54:18 +01:00
Mathieu
9c3d39b4a7 feat: technical release (#6648) 2023-01-31 11:18:19 +01:00
Mathieu
28800f43ee fix(lite): use browser timestamps for stats (#6623) 2023-01-31 10:26:07 +01:00
Gabriel Gunullu
5c0b29c51f feat(xo-web/network): NBD option (#6646) 2023-01-30 17:34:21 +01:00
Gabriel Gunullu
62d9d0208b feat(xo-server/network.set): support (un)setting NBD (#6635) 2023-01-30 16:02:28 +01:00
Pierre Donias
4bf871e52f fix(lite): stats.memory is undefined (#6647)
Introduced by 4f31b7007a
2023-01-30 14:40:57 +01:00
Florent BEAUCHAMP
103972808c fix(xo-vmdk-to-vhd): better computation of overprovisioning of very sparse disks (#6639) 2023-01-30 14:15:44 +01:00
Julien Fontanet
dc65bb87b5 feat(upload-ova): special handling of invalid params error (#6626)
Fixes #6622

Similar to 036b30212 & 65daa39eb
2023-01-30 14:09:28 +01:00
Mathieu
bfa0282ecc feat: technical release (#6645) 2023-01-27 16:16:26 +01:00
Mathieu
aa66ec0ccd fix(changelog): fix release type on a package (#6644) 2023-01-27 15:09:10 +01:00
Mathieu
18fe19c680 fix(lite): fix getHostMemoryFunction error (#6643) 2023-01-27 14:43:11 +01:00
Julien Fontanet
ab0e411ac0 chore(xo-server/rest-api): improve code
- mutualize object fetching
- mutualize error handling
2023-01-27 13:01:21 +01:00
Pierre Donias
79671e6d61 fix(lite/build): "Big integer literals are not available in the configured target environment" (#6638)
Introduced by a281682f7a
2023-01-26 11:42:29 +01:00
Mathieu
71ad9773da feat(lite/vm): ability to change state of a VM (#6608) 2023-01-26 09:43:12 +01:00
Julien Fontanet
34ecc2bcbb feat(xo-server/rest-api): support setting name_label/name_description 2023-01-25 17:29:49 +01:00
Pierre Donias
53f4f265dc fix(xo-web/host/network): remove extra "mode" column (#6640)
Introduced by 7ede6bdbce
2023-01-25 17:19:03 +01:00
Florent BEAUCHAMP
97624ef836 fix(xo-vmdk-to-vhd): memory consumption during ova generation (#6637) 2023-01-25 10:23:50 +01:00
Julien Fontanet
fb8d0ed924 fix(xen-api/examples/import-vdi): fix tasks watching
Introduced by 3e351f852
2023-01-24 16:31:03 +01:00
Gabriel Gunullu
fedbdba13d feat(xo-web/recipes): static network config for k8s recipe (#6598) 2023-01-24 11:04:02 +01:00
Julien Fontanet
a281682f7a chore: update dev deps 2023-01-23 18:31:07 +01:00
Julien Fontanet
07e9f09692 docs(xo-server/rest-api): minor fix 2023-01-23 17:16:32 +01:00
Julien Fontanet
29d6e590de feat(xo-server/rest-api): support exporting VDI in raw format 2023-01-23 17:14:24 +01:00
Julien Fontanet
3e351f8529 feat(xen-api/examples/import-vdi): can create the VDI and various flags 2023-01-23 17:13:41 +01:00
Julien Fontanet
bfbfb9379a feat(xo-cli): improve no server message 2023-01-23 09:31:01 +01:00
rajaa-b
4f31b7007a feat(lite): RAM usage graph (#6604) 2023-01-20 11:44:54 +01:00
rajaa-b
fe0cc2ebb9 feat(lite): network throughput chart (#6610) 2023-01-19 16:10:36 +01:00
Mathieu
2fd6f521f8 feat(xo-web/licenses): make id and boundObjectId copyable (#6634) 2023-01-19 15:11:10 +01:00
Florent BEAUCHAMP
ec00728112 feat(xo-web): add toggle for viridian flag (#6631)
Fixes #6572
2023-01-19 09:33:36 +01:00
Julien Fontanet
7174c1edeb chore(xo-server/rest-api): doc fixes and changelog entry
Introduced by 7bd27e743
2023-01-18 23:43:57 +01:00
Julien Fontanet
7bd27e7437 feat(xo-server/rest-api): support to destroy VMs/VDIs 2023-01-18 23:35:49 +01:00
Florent BEAUCHAMP
0a28e30003 fix(xo-web): clarify windows update label (#6632)
Fix #6628
2023-01-18 17:31:28 +01:00
Mathieu
246c793c28 fix(xo-web/licenses): move message for XCP-ng license binding (#6630) 2023-01-18 17:11:21 +01:00
Florent BEAUCHAMP
5f0ea4d586 fix(xo-web): show bootable status for VM running pv_in_pvh virtualisation mode (#6629)
Fix #6432
2023-01-18 17:09:26 +01:00
Julien Fontanet
3c7d316b3c feat(xo-server): initial tasks infrastructure (#6625) 2023-01-17 16:12:04 +01:00
Julien Fontanet
645c8f32e3 chore(xo-server-perf-alert): use @xen-orchestra/log@0.5.0
Introduced by #6550
2023-01-17 15:42:38 +01:00
Gabriel Gunullu
adc5e7d0c0 test(vhd-cli): from Jest to test (#6605) 2023-01-17 10:39:41 +01:00
Thierry Goettelmann
b9b74ab1ac feat(lite/ui): first implementation of responsive UI (#6612) 2023-01-17 10:22:08 +01:00
Thierry Goettelmann
64298c04f2 feat(lite/ui): UiModal fix (#6617) 2023-01-17 09:25:29 +01:00
Gabriel Gunullu
3dfb7db039 chore(xo-server-perf-alert): print error (#6550) 2023-01-16 22:53:53 +01:00
Julien Fontanet
b64d8f5cbf fix(xo-server/rest-api): handle filter parsing errors 2023-01-16 17:34:23 +01:00
Julien Fontanet
c2e5225728 feat(xo-server): expose host.residentVms 2023-01-16 17:33:47 +01:00
Florent BEAUCHAMP
6c44a94bf4 fix(vhd-lib/parseVhdStream): also consume stream in NBD mode (#6613)
Consuming the stream is necessary for all writers including DeltaBackupWriter) otherwise other writers (e.g. DeltaBackupWriter i.e. CR) will stay stuck.
2023-01-16 10:54:45 +01:00
Florent BEAUCHAMP
a2d9310d0a fix(backups): fix size of NBD backups (#6599) 2023-01-16 10:43:29 +01:00
Julien Fontanet
05197b93ee feat(proxy): dedupe logs 2023-01-15 13:08:57 +01:00
Julien Fontanet
448d115d49 feat(xo-server): dedupe logs 2023-01-15 13:04:52 +01:00
Julien Fontanet
ae993dff45 feat(log/dedupe): helper to remove duplicated logs 2023-01-15 12:59:31 +01:00
Julien Fontanet
1bc4805f3d chore(log): move Log into own module 2023-01-15 12:59:31 +01:00
Julien Fontanet
98fe8f3955 chore(log): move createTransport into own module 2023-01-15 12:59:31 +01:00
Julien Fontanet
e902bcef67 chore(log): prefix internal modules by _ 2023-01-15 12:59:31 +01:00
Julien Fontanet
cb2a6e43a8 chore(log/utils.test.js): rename to _compileLogPattern.test.js 2023-01-15 12:59:31 +01:00
Julien Fontanet
b73a0992f8 feat(log): define public entry points
BREAKING CHANGE: Importing modules with extensions is now unsupported, i.e. use `@xen-orchestra/log/configure` instead of `@xen-orchestra/log/configure.js`.

Allows ESM modules to import modules without specifying extensions (just like CJS module) which will make migrating this lib to ESM painless in the future.
2023-01-15 12:58:35 +01:00
Julien Fontanet
d0b3d78639 feat(xo-server): round up host memory to nearest GiB
Fixes #5776

Improves the display of the value by ignoring the micro-kernel size (~50MiB), ie `128 GiB` instead of `127.96 GiB`.
2023-01-13 15:06:06 +01:00
Julien Fontanet
e6b8939772 fix(xapi/VM_snapshot): don't fail on NOBAK VDIs destruction failure 2023-01-12 15:25:09 +01:00
Julien Fontanet
bc372a982c fix(xapi/VM_checkpoint): remove unsupported ignoreNobakVdis 2023-01-12 15:20:40 +01:00
Florent Beauchamp
3ff8064f1b feat(backups): add more info about NBD backups in logs 2023-01-12 10:28:30 +01:00
Florent Beauchamp
834459186d fix(backups): useNbd must follow the config 2023-01-12 10:28:30 +01:00
Mathieu
12220ad4cf fix(lite/UsageBar): add color for dangerous cases (#6606) 2023-01-12 09:22:07 +01:00
Julien Fontanet
f6fd1db1ef feat(xo-server): increase HTTP server request timeout to 1 day
Fixes #6590
2023-01-11 22:07:35 +01:00
Julien Fontanet
a1050882ae docs(installation): explicit FreeBSD/OpenBSD not officially supported 2023-01-11 15:11:54 +01:00
Mathieu
687df5ead4 feat(lite/vm): change state button (#6571) 2023-01-11 10:51:16 +01:00
Mathieu
b057881ad0 fix(lite): fix type checking (#6607) 2023-01-10 16:16:32 +01:00
Julien Fontanet
2b23550996 chore(vhd-lib/createVhdStreamWithLength): use readChunkStrict
Related to zammad#10996

Not only it simplified the code a bit, but it also provides better error messages, especially on stream end.
2023-01-10 11:11:38 +01:00
Thierry Goettelmann
afeb20e589 fix(lite/Console): fix isReady condition (#6594) 2023-01-06 10:44:45 +01:00
Julien Fontanet
d7794518a2 chore: update to fs-extra@11 & parse-pairs@2 2023-01-05 11:33:09 +01:00
Julien Fontanet
fee61a43e3 chore: update to sinon@15 2023-01-05 11:16:03 +01:00
Julien Fontanet
b201afd192 chore: update dev deps 2023-01-05 10:21:06 +01:00
Florent Beauchamp
feef1f8b0a fix(backups/cleanVm): fix tests 2023-01-04 10:54:22 +01:00
Florent Beauchamp
1a5e2fde4f fix(vhd-lib/merge): require aliases for VHD directories 2023-01-04 10:54:22 +01:00
Julien Fontanet
609e957a55 fix: build script should build xo-server plugins
Introduced by 3bfd6c697
2023-01-04 10:53:55 +01:00
Thierry Goettelmann
5c18404174 feat(lite): update useCollectionFilter composable (#6538)
- Query String support must now be explicitly enabled with the `queryStringParam` option
- Added `initialFilters` option
- Added generic type support
- Updated documentation
2023-01-04 09:51:39 +01:00
Thierry Goettelmann
866a1dd8ae feat(lite): update useCollectionSorter composable (#6540)
- Query String support must now be explicitly enabled with the `queryStringParam` option
- Added `initialFilters` option
- Added generic type support
- Updated documentation
2023-01-04 09:42:51 +01:00
Julien Fontanet
3bfd6c6979 chore: use Turborepo to build
Why?

- ordering: build dependencies before dependents
- cache: don't rebuild if no changes in files or dependencies
- possibility to restrict to specific scopes

Changes:

- `yarn build` now only build `xo-server` and `xo-web` (and dependencies)
- `yarn build:xo-lite` build `@xen-orchestra/lite\ (and dependencies)
2023-01-03 11:39:20 +01:00
Florent BEAUCHAMP
06564e9091 feat(backups): remove merge limitations (#6591)
following #0635b3316ea077fccaa8b2d1e7a4d801eb701811
2023-01-03 11:07:07 +01:00
Thierry Goettelmann
1702783cfb feat(lite): Reactive chart theme (#6587) 2022-12-21 15:00:26 +01:00
rajaa-b
4ea0cbaa37 feat(xo-lite): Pool CPU usage chart (#6577) 2022-12-21 12:03:04 +01:00
Mathieu
2246e065f7 feat: release 5.78.0 (#6588) 2022-12-20 13:54:30 +01:00
253 changed files with 7207 additions and 3351 deletions

1
.gitignore vendored
View File

@@ -34,3 +34,4 @@ yarn-error.log.*
# code coverage
.nyc_output/
coverage/
.turbo/

View File

@@ -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

View File

@@ -33,7 +33,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^14.0.1",
"sinon": "^15.0.1",
"tap": "^16.3.0",
"test": "^3.2.1"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.3",
"version": "0.1.4",
"engines": {
"node": ">=8.10"
},
@@ -25,11 +25,11 @@
"dependencies": {
"@vates/multi-key-map": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/log": "^0.6.0",
"ensure-array": "^1.0.0"
},
"devDependencies": {
"sinon": "^14.0.1",
"sinon": "^15.0.1",
"test": "^3.2.1"
}
}

View File

@@ -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

View File

@@ -21,7 +21,7 @@
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.2.0"
"vhd-lib": "^4.2.1"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

54
@vates/task/.USAGE.md Normal file
View File

@@ -0,0 +1,54 @@
```js
import { Task } from '@vates/task'
const task = new Task({
name: 'my task',
// if defined, a new detached task is created
//
// if not defined and created inside an existing task, the new task is considered a subtask
onProgress(event) {
// this function is called each time this task or one of it's subtasks change state
const { id, timestamp, type } = event
if (type === 'start') {
const { name, parentId } = event
} else if (type === 'end') {
const { result, status } = event
} else if (type === 'info' || type === 'warning') {
const { data, message } = event
} else if (type === 'property') {
const { name, value } = event
}
},
})
// this field is settable once before being observed
task.id
task.status
await task.abort()
// if fn rejects, the task will be marked as failed
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)
// the abort signal of the current task if any, otherwise is `undefined`
Task.abortSignal
// sends an info on the current task if any, otherwise does nothing
Task.info(message, data)
// sends an info on the current task if any, otherwise does nothing
Task.warning(message, data)
// attaches a property to the current task if any, otherwise does nothing
//
// the latest value takes precedence
//
// examples:
// - progress
Task.set(property, value)
```

1
@vates/task/.npmignore Symbolic link
View File

@@ -0,0 +1 @@
../../scripts/npmignore

85
@vates/task/README.md Normal file
View File

@@ -0,0 +1,85 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/task
[![Package Version](https://badgen.net/npm/v/@vates/task)](https://npmjs.org/package/@vates/task) ![License](https://badgen.net/npm/license/@vates/task) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/task)](https://bundlephobia.com/result?p=@vates/task) [![Node compatibility](https://badgen.net/npm/node/@vates/task)](https://npmjs.org/package/@vates/task)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/task):
```sh
npm install --save @vates/task
```
## Usage
```js
import { Task } from '@vates/task'
const task = new Task({
name: 'my task',
// if defined, a new detached task is created
//
// if not defined and created inside an existing task, the new task is considered a subtask
onProgress(event) {
// this function is called each time this task or one of it's subtasks change state
const { id, timestamp, type } = event
if (type === 'start') {
const { name, parentId } = event
} else if (type === 'end') {
const { result, status } = event
} else if (type === 'info' || type === 'warning') {
const { data, message } = event
} else if (type === 'property') {
const { name, value } = event
}
},
})
// this field is settable once before being observed
task.id
task.status
await task.abort()
// if fn rejects, the task will be marked as failed
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)
// the abort signal of the current task if any, otherwise is `undefined`
Task.abortSignal
// sends an info on the current task if any, otherwise does nothing
Task.info(message, data)
// sends an info on the current task if any, otherwise does nothing
Task.warning(message, data)
// attaches a property to the current task if any, otherwise does nothing
//
// the latest value takes precedence
//
// examples:
// - progress
Task.set(property, value)
```
## 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)

184
@vates/task/index.js Normal file
View File

@@ -0,0 +1,184 @@
'use strict'
const assert = require('node:assert').strict
const { AsyncLocalStorage } = require('node:async_hooks')
// define a read-only, non-enumerable, non-configurable property
function define(object, property, value) {
Object.defineProperty(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 }
const asyncStorage = new AsyncLocalStorage()
const getTask = () => asyncStorage.getStore()
exports.Task = class Task {
static get abortSignal() {
const task = getTask()
if (task !== undefined) {
return task.#abortController.signal
}
}
static info(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('info', { data, message })
}
}
static run(opts, fn) {
return new this(opts).run(fn)
}
static set(name, value) {
const task = getTask()
if (task !== undefined) {
task.#emit('property', { name, value })
}
}
static warning(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('warning', { data, message })
}
}
static wrap(opts, fn) {
// compatibility with @decorateWith
if (typeof fn !== 'function') {
;[fn, opts] = [opts, fn]
}
return function taskRun() {
return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
}
}
#abortController = new AbortController()
#onProgress
#parent
get id() {
return (this.id = Math.random().toString(36).slice(2))
}
set id(value) {
define(this, 'id', value)
}
#startData
#status = PENDING
get status() {
return this.#status
}
constructor({ name, onProgress }) {
this.#startData = { name }
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)
})
this.#onProgress = parent.#onProgress
this.#startData.parentId = parent.id
} else {
this.#onProgress = noop
}
}
const { signal } = this.#abortController
signal.addEventListener('abort', () => {
if (this.status === PENDING) {
this.#status = this.#running ? ABORTING : ABORTED
}
})
}
abort(reason) {
this.#abortController.abort(reason)
}
#emit(type, data) {
data.id = this.id
data.timestamp = Date.now()
data.type = type
this.#onProgress(data)
}
#handleMaybeAbortion(result) {
if (this.status === ABORTING) {
this.#status = ABORTED
this.#emit('end', { status: ABORTED, result })
return true
}
return false;
}
async run(fn) {
const result = await this.runInside(fn)
if (this.status === PENDING) {
this.#status = SUCCESS
this.#emit('end', { status: SUCCESS, result })
}
return result
}
#running = false
async runInside(fn) {
assert.equal(this.status, PENDING)
assert.equal(this.#running, false)
this.#running = true
const startData = this.#startData
if (startData !== undefined) {
this.#startData = undefined
this.#emit('start', startData)
}
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 })
}
throw result
}
}
wrap(fn) {
const task = this
return function taskRun() {
return task.run(() => fn.apply(this, arguments))
}
}
wrapInside(fn) {
const task = this
return function taskRunInside() {
return task.runInside(() => fn.apply(this, arguments))
}
}
}

23
@vates/task/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"private": false,
"name": "@vates/task",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/task",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/task",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.0.1",
"engines": {
"node": ">=14"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -35,7 +35,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^14.0.1",
"sinon": "^15.0.1",
"test": "^3.2.1"
}
}

View File

@@ -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

View File

@@ -7,7 +7,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.2.2",
"version": "0.2.3",
"engines": {
"node": ">=14"
},
@@ -17,7 +17,7 @@
},
"dependencies": {
"@vates/decorate-with": "^2.0.0",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/log": "^0.6.0",
"golike-defer": "^0.5.1",
"object-hash": "^2.0.1"
},

View File

@@ -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

View File

@@ -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.4",
"@xen-orchestra/fs": "^3.3.0",
"@xen-orchestra/backups": "^0.29.5",
"@xen-orchestra/fs": "^3.3.1",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",

View File

@@ -38,7 +38,7 @@ const DEFAULT_VM_SETTINGS = {
fullInterval: 0,
healthCheckSr: undefined,
healthCheckVmsWithTags: [],
maxMergedDeltasPerRun: 2,
maxMergedDeltasPerRun: Infinity,
offlineBackup: false,
offlineSnapshot: false,
snapshotRetention: 0,

View File

@@ -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

View File

@@ -28,6 +28,7 @@ const { isMetadataFile } = require('./_backupType.js')
const { isValidXva } = require('./_isValidXva.js')
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
const { lvs, pvs } = require('./_lvm.js')
const { watchStreamSize } = require('./_watchStreamSize')
// @todo : this import is marked extraneous , sould be fixed when lib is published
const { mount } = require('@vates/fuse-vhd')
const { asyncEach } = require('@vates/async-each')
@@ -661,7 +662,7 @@ class RemoteAdapter {
const handler = this._handler
if (this.#useVhdDirectory()) {
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
await createVhdDirectoryFromStream(handler, dataPath, input, {
const size = await createVhdDirectoryFromStream(handler, dataPath, input, {
concurrency: writeBlockConcurrency,
compression: this.#getCompressionType(),
async validator() {
@@ -671,12 +672,14 @@ class RemoteAdapter {
nbdClient,
})
await VhdAbstract.createAlias(handler, path, dataPath)
return size
} else {
await this.outputStream(path, input, { checksum, validator })
return this.outputStream(path, input, { checksum, validator })
}
}
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
const container = watchStreamSize(input)
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
@@ -685,6 +688,7 @@ class RemoteAdapter {
return validator.apply(this, arguments)
},
})
return container.size
}
// open the hierarchy of ancestors until we find a full one

View File

@@ -100,7 +100,7 @@ class Task {
* In case of error, the task will be failed.
*
* @typedef Result
* @param {() => Result)} fn
* @param {() => Result} fn
* @param {boolean} last - Whether the task should succeed if there is no error
* @returns Result
*/

View File

@@ -1,6 +1,6 @@
'use strict'
require('@xen-orchestra/log/configure.js').catchGlobalErrors(
require('@xen-orchestra/log/configure').catchGlobalErrors(
require('@xen-orchestra/log').createLogger('xo:backups:worker')
)

View File

@@ -31,7 +31,7 @@ beforeEach(async () => {
})
afterEach(async () => {
await pFromCallback(cb => rimraf(tempDir, cb))
await rimraf(tempDir)
await handler.forget()
})
@@ -221,7 +221,7 @@ test('it merges delta of non destroyed chain', async () => {
loggued.push(message)
}
await adapter.cleanVm(rootPath, { remove: true, logInfo, logWarn: logInfo, lock: false })
assert.equal(loggued[0], `incorrect backup size in metadata`)
assert.equal(loggued[0], `unexpected number of entries in backup cache`)
loggued = []
await adapter.cleanVm(rootPath, { remove: true, merge: true, logInfo, logWarn: () => {}, lock: false })
@@ -378,7 +378,19 @@ describe('tests multiple combination ', () => {
],
})
)
if (!useAlias && vhdMode === 'directory') {
try {
await adapter.cleanVm(rootPath, { remove: true, merge: true, logWarn: () => {}, lock: false })
} catch (err) {
assert.strictEqual(
err.code,
'NOT_SUPPORTED',
'Merging directory without alias should raise a not supported error'
)
return
}
assert.strictEqual(true, false, 'Merging directory without alias should raise an error')
}
await adapter.cleanVm(rootPath, { remove: true, merge: true, logWarn: () => {}, lock: false })
const metadata = JSON.parse(await handler.readFile(`${rootPath}/metadata.json`))

View File

@@ -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

View File

@@ -4,7 +4,7 @@
'use strict'
const { catchGlobalErrors } = require('@xen-orchestra/log/configure.js')
const { catchGlobalErrors } = require('@xen-orchestra/log/configure')
const { createLogger } = require('@xen-orchestra/log')
const { getSyncedHandler } = require('@xen-orchestra/fs')
const { join } = require('path')

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.29.4",
"version": "0.29.5",
"engines": {
"node": ">=14.6"
},
@@ -21,38 +21,38 @@
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.3",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^1.0.0",
"@vates/nbd-client": "*",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^3.3.0",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/fs": "^3.3.1",
"@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": "^10.0.0",
"fs-extra": "^11.1.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",
"lodash": "^4.17.20",
"node-zone": "^0.4.0",
"parse-pairs": "^1.1.0",
"parse-pairs": "^2.0.0",
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"uuid": "^9.0.0",
"vhd-lib": "^4.2.0",
"vhd-lib": "^4.2.1",
"yazl": "^2.5.1"
},
"devDependencies": {
"rimraf": "^3.0.2",
"sinon": "^14.0.1",
"rimraf": "^4.1.1",
"sinon": "^15.0.1",
"test": "^3.2.1",
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^1.6.0"
"@xen-orchestra/xapi": "^1.6.1"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -20,7 +20,7 @@ const { packUuid } = require('./_packUuid.js')
const { Disposable } = require('promise-toolbox')
const NbdClient = require('@vates/nbd-client')
const { debug, warn } = createLogger('xo:backups:DeltaBackupWriter')
const { debug, warn, info } = createLogger('xo:backups:DeltaBackupWriter')
exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
async checkBaseVdis(baseUuidToSrcVdi) {
@@ -133,7 +133,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
}
}
async _transfer({ timestamp, deltaExport, sizeContainers }) {
async _transfer({ timestamp, deltaExport }) {
const adapter = this._adapter
const backup = this._backup
@@ -172,6 +172,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
}
const { size } = await Task.run({ name: 'transfer' }, async () => {
let transferSize = 0
await Promise.all(
map(deltaExport.vdis, async (vdi, id) => {
const path = `${this._vmBackupDir}/${vhds[id]}`
@@ -200,21 +201,25 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
const vdiRef = vm.$xapi.getObject(vdi.uuid).$ref
let nbdClient
if (!this._backup.config.useNbd) {
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()
debug(`got nbd connection `, { vdi: vdi.uuid })
info('NBD client ready', { vdi: id, path })
} catch (error) {
nbdClient = undefined
debug(`can't connect to nbd server or no server available`, { error })
warn('error connecting to NBD server', { error, vdi: id, path })
}
} else {
debug('useNbd is disabled', { vdi: id, path })
}
await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
// no checksum for VHDs, because they will be invalidated by
// merges and chainings
checksum: false,
@@ -235,9 +240,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
})
})
)
return {
size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
}
return { size: transferSize }
})
metadataContent.size = size
this._metadataFileName = await adapter.writeVmBackupMetadata(vm.uuid, metadataContent)

View File

@@ -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

View File

@@ -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

View File

@@ -42,7 +42,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^14.0.1",
"sinon": "^15.0.1",
"test": "^3.2.1"
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "3.3.0",
"version": "3.3.1",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
@@ -30,11 +30,11 @@
"@vates/decorate-with": "^2.0.0",
"@vates/read-chunk": "^1.0.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/log": "^0.6.0",
"bind-property-descriptor": "^2.0.0",
"decorator-synchronized": "^0.6.0",
"execa": "^5.0.0",
"fs-extra": "^10.0.0",
"fs-extra": "^11.1.0",
"get-stream": "^6.0.0",
"limit-concurrency-decorator": "^0.5.0",
"lodash": "^4.17.4",
@@ -53,7 +53,7 @@
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"dotenv": "^16.0.0",
"rimraf": "^3.0.0",
"rimraf": "^4.1.1",
"tmp": "^0.2.1"
},
"scripts": {

View File

@@ -116,7 +116,7 @@ describe('encryption', () => {
dir = await pFromCallback(cb => tmp.dir(cb))
})
afterAll(async () => {
await pFromCallback(cb => rimraf(dir, cb))
await rimraf(dir)
})
it('sync should NOT create metadata if missing (not encrypted)', async () => {

View File

@@ -7,6 +7,10 @@
- Uncollapse hosts in the tree by default (PR [#6428](https://github.com/vatesfr/xen-orchestra/pull/6428))
- Display RAM usage in pool dashboard (PR [#6419](https://github.com/vatesfr/xen-orchestra/pull/6419))
- Implement not found page (PR [#6410](https://github.com/vatesfr/xen-orchestra/pull/6410))
- Display CPU usage chart in pool dashboard (PR [#6577](https://github.com/vatesfr/xen-orchestra/pull/6577))
- 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))
## **0.1.0**

View File

@@ -49,6 +49,7 @@
"eslint-plugin-vue": "^9.0.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.19",
"postcss-custom-media": "^9.0.1",
"postcss-nested": "^6.0.0",
"typescript": "^4.9.3",
"vite": "^3.2.4",

View File

@@ -1,5 +1,6 @@
module.exports = {
plugins: {
"postcss-nested": {},
"postcss-custom-media": {},
},
};

View File

@@ -26,9 +26,9 @@
<div v-else>
<AppHeader />
<div style="display: flex">
<nav class="nav">
<InfraPoolList />
</nav>
<transition name="slide">
<AppNavigation />
</transition>
<main class="main">
<RouterView />
</main>
@@ -38,6 +38,7 @@
</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";
@@ -48,7 +49,6 @@ import { faServer } from "@fortawesome/free-solid-svg-icons";
import AppHeader from "@/components/AppHeader.vue";
import AppLogin from "@/components/AppLogin.vue";
import AppTooltips from "@/components/AppTooltips.vue";
import InfraPoolList from "@/components/infra/InfraPoolList.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import { useChartTheme } from "@/composables/chart-theme.composable";
@@ -118,14 +118,14 @@ const reload = () => window.location.reload();
<style lang="postcss">
@import "@/assets/base.css";
.nav {
overflow: auto;
width: 37rem;
max-width: 37rem;
height: calc(100vh - 9rem);
padding: 0.5rem;
border-right: 1px solid var(--color-blue-scale-400);
background-color: var(--background-color-primary);
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
transform: translateX(-37rem);
}
.main {

View File

@@ -0,0 +1,2 @@
@custom-media --mobile (max-width: 1023px);
@custom-media --desktop (min-width: 1024px);

View File

@@ -1,5 +1,11 @@
<template>
<header class="app-header">
<UiIcon
v-if="isMobile"
ref="navigationTrigger"
:icon="faBars"
class="toggle-navigation"
/>
<RouterLink :to="{ name: 'home' }">
<img alt="XO Lite" src="../assets/logo.svg" />
</RouterLink>
@@ -12,6 +18,17 @@
<script lang="ts" setup>
import AccountButton from "@/components/AccountButton.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useUiStore } from "@/stores/ui.store";
import { faBars } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
const uiStore = useUiStore();
const { isMobile } = storeToRefs(uiStore);
const navigationStore = useNavigationStore();
const { trigger: navigationTrigger } = storeToRefs(navigationStore);
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,57 @@
<template>
<nav
v-if="isDesktop || isOpen"
ref="navElement"
:class="{ collapsible: isMobile }"
class="app-navigation"
>
<InfraPoolList />
</nav>
</template>
<script lang="ts" setup>
import InfraPoolList from "@/components/infra/InfraPoolList.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useUiStore } from "@/stores/ui.store";
import { onClickOutside, whenever } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { ref } from "vue";
const uiStore = useUiStore();
const { isMobile, isDesktop } = storeToRefs(uiStore);
const navigationStore = useNavigationStore();
const { isOpen, trigger } = storeToRefs(navigationStore);
const navElement = ref();
whenever(isOpen, () => {
const unregisterEvent = onClickOutside(
navElement,
() => {
isOpen.value = false;
unregisterEvent?.();
},
{
ignore: [trigger],
}
);
});
</script>
<style lang="postcss" scoped>
.app-navigation {
overflow: auto;
width: 37rem;
max-width: 37rem;
height: calc(100vh - 9rem);
padding: 0.5rem;
border-right: 1px solid var(--color-blue-scale-400);
background-color: var(--background-color-primary);
&.collapsible {
position: fixed;
z-index: 1;
}
}
</style>

View File

@@ -66,7 +66,7 @@ import useModal from "@/composables/modal.composable";
defineProps<{
availableSorts: Sorts;
activeSorts: ActiveSorts;
activeSorts: ActiveSorts<Record<string, any>>;
}>();
const emit = defineEmits<{

View File

@@ -66,9 +66,11 @@ const emit = defineEmits<{
const isSelectable = computed(() => props.modelValue !== undefined);
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter({
queryStringParam: "filter",
});
const { sorts, addSort, removeSort, toggleSortDirection, compareFn } =
useCollectionSorter();
useCollectionSorter<Record<string, any>>({ queryStringParam: "sort" });
const filteredCollection = useFilteredCollection(
toRef(props, "collection"),

View File

@@ -0,0 +1,23 @@
<template>
<span :title="date.toLocaleString()">{{ relativeTime }}</span>
</template>
<script lang="ts" setup>
import useRelativeTime from "@/composables/relative-time.composable";
import { useNow } from "@vueuse/core";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
date: Date | number | string;
interval?: number;
}>(),
{ interval: 1000 }
);
const date = computed(() => new Date(props.date));
const now = useNow({ interval: props.interval });
const relativeTime = useRelativeTime(date, now);
</script>
<style lang="postcss" scoped></style>

View File

@@ -11,5 +11,7 @@
height: 6.5rem;
background-color: var(--background-color-primary);
border-bottom: 1px solid var(--color-blue-scale-400);
max-width: 100%;
overflow: auto;
}
</style>

View File

@@ -19,6 +19,10 @@ defineProps<{
</script>
<style lang="postcss" scoped>
.actions {
margin-left: auto;
}
.title-bar {
display: flex;
align-items: center;

View File

@@ -5,6 +5,10 @@
v-for="item in computedData.sortedArray"
:key="item.id"
class="progress-item"
:class="{
warning: item.value > MIN_WARNING_VALUE,
error: item.value > MIN_DANGEROUS_VALUE,
}"
>
<UiProgressBar :value="item.value" color="custom" />
<div class="legend">
@@ -15,9 +19,7 @@
}}</UiBadge>
</div>
</div>
<div class="footer">
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
</div>
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
</template>
<UiSpinner v-else class="spinner" />
</div>
@@ -42,6 +44,9 @@ interface Props {
nItems?: number;
}
const MIN_WARNING_VALUE = 80;
const MIN_DANGEROUS_VALUE = 90;
const props = defineProps<Props>();
const computedData = computed(() => {
@@ -64,15 +69,7 @@ const computedData = computed(() => {
});
</script>
<style scoped>
.footer {
display: flex;
justify-content: space-between;
font-weight: 700;
font-size: 14px;
color: var(--color-blue-scale-300);
}
<style lang="postcss" scoped>
.spinner {
color: var(--color-extra-blue-base);
display: flex;
@@ -94,12 +91,6 @@ const computedData = computed(() => {
font-weight: 700;
}
.progress-item {
--progress-bar-height: 1.2rem;
--progress-bar-color: var(--color-extra-blue-l20);
--progress-bar-background-color: var(--color-blue-scale-400);
}
.progress-item:nth-child(1) {
--progress-bar-color: var(--color-extra-blue-d60);
}
@@ -112,6 +103,18 @@ const computedData = computed(() => {
--progress-bar-color: var(--color-extra-blue-d20);
}
.progress-item {
--progress-bar-height: 1.2rem;
--progress-bar-color: var(--color-extra-blue-l20);
--progress-bar-background-color: var(--color-blue-scale-400);
&.warning {
--progress-bar-color: var(--color-orange-world-base);
}
&.error {
--progress-bar-color: var(--color-red-vates-base);
}
}
.circle {
display: inline-block;
width: 1rem;

View File

@@ -18,15 +18,15 @@ const data: LinearChartData = [
{
label: "First series",
data: [
{ date: "...", value: 1234 },
{ date: "...", value: 1234 },
{ timestamp: 1670478371123, value: 1234 },
{ timestamp: 1670478519751, value: 1234 },
],
},
{
label: "Second series",
data: [
{ date: "...", value: 1234 },
{ date: "...", value: 1234 },
{ timestamp: 1670478519751, value: 1234 },
{ timestamp: 167047555000, value: 1234 },
],
},
];

View File

@@ -6,6 +6,7 @@
</template>
<script lang="ts" setup>
import { utcFormat } from "d3-time-format";
import type { EChartsOption } from "echarts";
import { computed, provide } from "vue";
import VueCharts from "vue-echarts";
@@ -22,20 +23,18 @@ import { CanvasRenderer } from "echarts/renderers";
import type { OptionDataValue } from "echarts/types/src/util/types";
import UiCard from "@/components/ui/UiCard.vue";
const Y_AXIS_MAX_VALUE = 200;
const props = defineProps<{
title?: string;
subtitle?: string;
data: LinearChartData;
valueFormatter?: (value: number) => string;
maxValue?: number;
}>();
const valueFormatter = (value: OptionDataValue | OptionDataValue[]) => {
if (props.valueFormatter) {
return props.valueFormatter(value as number);
}
return value.toString();
};
const valueFormatter = (value: OptionDataValue | OptionDataValue[]) =>
props.valueFormatter?.(value as number) ?? `${value}`;
provide("valueFormatter", valueFormatter);
@@ -62,8 +61,10 @@ const option = computed<EChartsOption>(() => ({
xAxis: {
type: "time",
axisLabel: {
showMinLabel: true,
showMaxLabel: true,
formatter: (timestamp: number) =>
utcFormat("%a\n%I:%M\n%p")(new Date(timestamp)),
showMaxLabel: false,
showMinLabel: false,
},
},
yAxis: {
@@ -71,19 +72,20 @@ const option = computed<EChartsOption>(() => ({
axisLabel: {
formatter: valueFormatter,
},
max: () => props.maxValue ?? Y_AXIS_MAX_VALUE,
},
series: props.data.map((series, index) => ({
type: "line",
name: series.label,
zlevel: index + 1,
data: series.data.map((item) => [item.date, item.value]),
data: series.data.map((item) => [item.timestamp, item.value]),
})),
}));
</script>
<style lang="postcss" scoped>
.chart {
width: 50rem;
width: 100%;
height: 30rem;
}
</style>

View File

@@ -12,7 +12,13 @@
</MenuTrigger>
<AppMenu v-else shadow :disabled="isDisabled">
<template #trigger="{ open, isOpen }">
<MenuTrigger :active="isOpen" :icon="icon" @click="open">
<MenuTrigger
:active="isOpen"
:busy="isBusy"
:disabled="isDisabled"
:icon="icon"
@click="open"
>
<slot />
<UiIcon
:fixed-width="false"

View File

@@ -0,0 +1,93 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('network-throughput')"
:value-formatter="customValueFormatter"
/>
</template>
<script lang="ts" setup>
import { computed, inject } from "vue";
import { map } from "lodash-es";
import { useI18n } from "vue-i18n";
import LinearChart from "@/components/charts/LinearChart.vue";
import type { FetchedStats } from "@/composables/fetch-stats.composable";
import { formatSize } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import type { LinearChartData } from "@/types/chart";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { XenApiHost } from "@/libs/xen-api";
const { t } = useI18n();
const hostLastWeekStats =
inject<FetchedStats<XenApiHost, HostStats>>("hostLastWeekStats");
const data = computed<LinearChartData>(() => {
const stats = hostLastWeekStats?.stats?.value;
const timestampStart = hostLastWeekStats?.timestampStart?.value;
if (timestampStart === undefined || stats === undefined) {
return [];
}
const results = {
tx: new Map<number, { timestamp: number; value: number }>(),
rx: new Map<number, { timestamp: number; value: number }>(),
};
const addResult = (stats: HostStats, type: "tx" | "rx") => {
const networkStats = Object.values(stats.pifs[type]);
for (let hourIndex = 0; hourIndex < networkStats[0].length; hourIndex++) {
const timestamp =
(timestampStart + hourIndex * RRD_STEP_FROM_STRING.hours) * 1000;
const networkThroughput = networkStats.reduce(
(total, throughput) => total + throughput[hourIndex],
0
);
results[type].set(timestamp, {
timestamp,
value: (results[type].get(timestamp)?.value ?? 0) + networkThroughput,
});
}
};
stats.forEach((host) => {
if (!host.stats) {
return;
}
addResult(host.stats, "rx");
addResult(host.stats, "tx");
});
return [
{
label: t("network-upload"),
data: Array.from(results["tx"].values()),
},
{
label: t("network-download"),
data: Array.from(results["rx"].values()),
},
];
});
// TODO: improve the way to get the max value of graph
// See: https://github.com/vatesfr/xen-orchestra/pull/6610/files#r1072237279
const customMaxValue = computed(
() =>
Math.max(
...map(data.value[0].data, "value"),
...map(data.value[1].data, "value")
) * 1.5
);
const customValueFormatter = (value: number) => String(formatSize(value));
</script>

View File

@@ -8,25 +8,8 @@
:data="srStore.isReady ? data.result : undefined"
:nItems="N_ITEMS"
>
<template #footer v-if="showFooter">
<div class="footer-card">
<p>{{ $t("total-used") }}:</p>
<div class="footer-value">
<p>{{ percentUsed }}%</p>
<p>
{{ formatSize(data.usedSize) }}
</p>
</div>
</div>
<div class="footer-card">
<p>{{ $t("total-free") }}:</p>
<div class="footer-value">
<p>{{ percentFree }}%</p>
<p>
{{ formatSize(data.maxSize) }}
</p>
</div>
</div>
<template #footer>
<SizeStatsSummary :size="data.maxSize" :usage="data.usedSize" />
</template>
</UsageBar>
</UiCard>
@@ -35,24 +18,14 @@
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { computed } from "vue";
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import UsageBar from "@/components/UsageBar.vue";
import UiCard from "@/components/ui/UiCard.vue";
import { formatSize, percent } from "@/libs/utils";
import { useSrStore } from "@/stores/storage.store";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const srStore = useSrStore();
const percentUsed = computed(() =>
percent(data.value.usedSize, data.value.maxSize, 1)
);
const percentFree = computed(() =>
percent(data.value.maxSize - data.value.usedSize, data.value.maxSize, 1)
);
const showFooter = computed(() => !isNaN(percentUsed.value));
const data = computed<{
result: { id: string; label: string; value: number }[];
maxSize: number;
@@ -87,21 +60,3 @@ const data = computed<{
return { result, maxSize, usedSize };
});
</script>
<style lang="postcss" scoped>
.footer-card {
color: var(--color-blue-scale-200);
display: flex;
text-transform: uppercase;
}
.footer-card p {
font-weight: 700;
}
.footer-value {
display: flex;
flex-direction: column;
text-align: right;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-cpu-usage')"
:value-formatter="customValueFormatter"
/>
</template>
<script lang="ts" setup>
import LinearChart from "@/components/charts/LinearChart.vue";
import type { HostStats } from "@/libs/xapi-stats";
import type { FetchedStats } from "@/composables/fetch-stats.composable";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { XenApiHost } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData } from "@/types/chart";
import { sumBy } from "lodash-es";
import { storeToRefs } from "pinia";
import { computed, inject } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const hostLastWeekStats =
inject<FetchedStats<XenApiHost, HostStats>>("hostLastWeekStats");
const { allRecords: hosts } = storeToRefs(useHostStore());
const customMaxValue = computed(
() => 100 * sumBy(hosts.value, (host) => +host.cpu_info.cpu_count)
);
const data = computed<LinearChartData>(() => {
const timestampStart = hostLastWeekStats?.timestampStart?.value;
const stats = hostLastWeekStats?.stats?.value;
if (timestampStart === undefined || stats === undefined) {
return [];
}
const result = new Map<number, { timestamp: number; value: number }>();
const addResult = (stats: HostStats) => {
const cpus = Object.values(stats.cpus);
for (let hourIndex = 0; hourIndex < cpus[0].length; hourIndex++) {
const timestamp =
(timestampStart + hourIndex * RRD_STEP_FROM_STRING.hours) * 1000;
const cpuUsageSum = cpus.reduce(
(total, cpu) => total + cpu[hourIndex],
0
);
result.set(timestamp, {
timestamp: timestamp,
value: Math.round((result.get(timestamp)?.value ?? 0) + cpuUsageSum),
});
}
};
stats.forEach((host) => {
if (!host.stats) {
return;
}
addResult(host.stats);
});
return [
{
label: t("stacked-cpu-usage"),
data: Array.from(result.values()),
},
];
});
const customValueFormatter = (value: number) => `${value}%`;
</script>

View File

@@ -0,0 +1,93 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-ram-usage')"
:value-formatter="customValueFormatter"
>
<template #summary>
<SizeStatsSummary :size="currentData.size" :usage="currentData.usage" />
</template>
</LinearChart>
</template>
<script lang="ts" setup>
import LinearChart from "@/components/charts/LinearChart.vue";
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import type { FetchedStats } from "@/composables/fetch-stats.composable";
import type { HostStats } from "@/libs/xapi-stats";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData } from "@/types/chart";
import { sumBy } from "lodash-es";
import { storeToRefs } from "pinia";
import { computed, inject } from "vue";
import { useI18n } from "vue-i18n";
import { formatSize, getHostMemory, isHostRunning } from "@/libs/utils";
import type { XenApiHost } from "@/libs/xen-api";
const { allRecords: hosts } = storeToRefs(useHostStore());
const { t } = useI18n();
const hostLastWeekStats =
inject<FetchedStats<XenApiHost, HostStats>>("hostLastWeekStats");
const runningHosts = computed(() => hosts.value.filter(isHostRunning));
const customMaxValue = computed(() =>
sumBy(runningHosts.value, (host) => getHostMemory(host)?.size ?? 0)
);
const currentData = computed(() => {
let size = 0,
usage = 0;
runningHosts.value.forEach((host) => {
const hostMemory = getHostMemory(host);
size += hostMemory?.size ?? 0;
usage += hostMemory?.usage ?? 0;
});
return { size, usage };
});
const data = computed<LinearChartData>(() => {
const timestampStart = hostLastWeekStats?.timestampStart?.value;
const stats = hostLastWeekStats?.stats?.value;
if (timestampStart === undefined || stats === undefined) {
return [];
}
const result = new Map<number, { timestamp: number; value: number }>();
stats.forEach(({ stats }) => {
if (stats?.memory === undefined) {
return;
}
const memoryFree = stats.memoryFree;
const memoryUsage = stats.memory.map(
(memory, index) => memory - memoryFree[index]
);
memoryUsage.forEach((value, hourIndex) => {
const timestamp =
(timestampStart + hourIndex * RRD_STEP_FROM_STRING.hours) * 1000;
result.set(timestamp, {
timestamp,
value: (result.get(timestamp)?.value ?? 0) + memoryUsage[hourIndex],
});
});
});
return [
{
label: t("stacked-ram-usage"),
data: Array.from(result.values()),
},
];
});
const customValueFormatter = (value: number) => String(formatSize(value));
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="summary" v-if="isDisplayed">
<div class="summary-card">
<p>{{ $t("total-used") }}:</p>
<div class="summary-value">
<p>{{ percentUsed }}%</p>
<p>
{{ formatSize(usage) }}
</p>
</div>
</div>
<div class="summary-card">
<p>{{ $t("total-free") }}:</p>
<div class="summary-value">
<p>{{ percentFree }}%</p>
<p>
{{ formatSize(free) }}
</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { formatSize, percent } from "@/libs/utils";
import { computed } from "vue";
const props = defineProps<{
size: number;
usage: number;
}>();
const free = computed(() => props.size - props.usage);
const percentFree = computed(() => percent(free.value, props.size));
const percentUsed = computed(() => percent(props.usage, props.size));
const isDisplayed = computed(
() => !isNaN(percentUsed.value) && !isNaN(percentFree.value)
);
</script>
<style lang="postcss" scoped>
.summary {
display: flex;
justify-content: space-between;
font-weight: 700;
font-size: 14px;
color: var(--color-blue-scale-300);
}
.summary-card {
color: var(--color-blue-scale-200);
display: flex;
text-transform: uppercase;
}
.summary-card p {
font-weight: 700;
}
.summary-value {
display: flex;
flex-direction: column;
text-align: right;
}
</style>

View File

@@ -1,14 +1,15 @@
<template>
<div class="ui-key-value-list"><slot /></div>
<table class="ui-key-value-list">
<tbody>
<slot />
</tbody>
</table>
</template>
<script lang="ts" setup></script>
<style lang="postcss" scoped>
.ui-key-value-list {
margin-top: 2rem;
font-size: 1.4rem;
/* UiKeyValueRow: 15em (key) + 15em (value) + 1rem (gap) */
min-width: calc(30em + 1rem);
border-spacing: 0;
}
</style>

View File

@@ -1,37 +1,33 @@
<template>
<div class="ui-key-value-row">
<span class="key" v-if="$slots.key">
<tr class="ui-key-value-row">
<th v-if="$slots.key" class="key">
<slot name="key" />
</span>
<span class="value">
</th>
<td :colspan="$slots.key ? 1 : 2" class="value">
<slot name="value" />
</span>
</div>
</td>
</tr>
</template>
<script lang="ts" setup></script>
<style lang="postcss" scoped>
.ui-key-value-row {
display: flex;
gap: 1rem;
align-items: baseline;
margin: 0.5em 0;
}
.key {
color: var(--color-blue-scale-300);
width: 15%;
max-width: 15em;
max-width: 30em;
}
.value {
flex-grow: 1;
}
@import "@/assets/_responsive.pcss";
.key,
.value {
text-overflow: ellipsis;
width: 100%;
min-width: 15em;
max-width: 30em;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-weight: 400;
}
.key {
padding-right: 2rem;
text-align: left;
color: var(--color-blue-scale-300);
@media (--desktop) {
min-width: 20rem;
}
}
</style>

View File

@@ -33,12 +33,12 @@
</template>
<script lang="ts" setup>
import { computed } from "vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useMagicKeys, whenever } from "@vueuse/core";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
@@ -64,11 +64,13 @@ const className = computed(() => {
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background-color: #00000080;
@@ -142,6 +144,9 @@ const className = computed(() => {
}
.content {
overflow: auto;
min-height: 23rem;
max-height: calc(100vh - 40rem);
margin-top: 2rem;
}

View File

@@ -0,0 +1,181 @@
<template>
<TitleBar :icon="faDisplay">
{{ name }}
<template #actions>
<AppMenu shadow placement="bottom-end">
<template #trigger="{ open, isOpen }">
<UiButton :active="isOpen" :icon="faPowerOff" @click="open">
{{ $t("change-state") }}
<UiIcon :icon="faAngleDown" />
</UiButton>
</template>
<MenuItem
@click="xenApi.vm.start({ vmRef: vm.$ref })"
:busy="isOperationsPending('start')"
:disabled="!isHalted"
:icon="faPlay"
>
{{ $t("start") }}
</MenuItem>
<MenuItem
:busy="isOperationsPending('start_on')"
:disabled="!isHalted"
:icon="faServer"
>
{{ $t("start-on-host") }}
<template #submenu>
<MenuItem
v-for="host in hostStore.allRecords"
@click="xenApi.vm.startOn({ vmRef: vm.$ref, hostRef: host.$ref })"
v-bind:key="host.$ref"
:icon="faServer"
>
<div class="wrapper">
{{ host.name_label }}
<div>
<UiIcon
:icon="
host.$ref === poolStore.pool?.master ? faStar : undefined
"
class="star"
/>
<PowerStateIcon
:state="isHostRunning(host) ? 'Running' : 'Halted'"
/>
</div>
</div>
</MenuItem>
</template>
</MenuItem>
<MenuItem
@click="xenApi.vm.pause({ vmRef: vm.$ref })"
:busy="isOperationsPending('pause')"
:disabled="!isRunning"
:icon="faPause"
>
{{ $t("pause") }}
</MenuItem>
<MenuItem
@click="xenApi.vm.suspend({ vmRef: vm.$ref })"
:busy="isOperationsPending('suspend')"
:disabled="!isRunning"
:icon="faMoon"
>
{{ $t("suspend") }}
</MenuItem>
<!-- TODO: update the icon once Clémence has integrated the action into figma -->
<MenuItem
@click="
xenApi.vm.resume({
vmRef: vm.$ref,
})
"
:busy="isOperationsPending(['unpause', 'resume'])"
:disabled="!isSuspended && !isPaused"
:icon="faCirclePlay"
>
{{ $t("resume") }}
</MenuItem>
<MenuItem
@click="xenApi.vm.reboot({ vmRef: vm.$ref })"
:busy="isOperationsPending('clean_reboot')"
:disabled="!isRunning"
:icon="faRotateLeft"
>
{{ $t("reboot") }}
</MenuItem>
<MenuItem
@click="xenApi.vm.reboot({ vmRef: vm.$ref, force: true })"
:busy="isOperationsPending('hard_reboot')"
:disabled="!isRunning && !isPaused"
:icon="faRepeat"
>
{{ $t("force-reboot") }}
</MenuItem>
<MenuItem
@click="xenApi.vm.shutdown({ vmRef: vm.$ref })"
:busy="isOperationsPending('clean_shutdown')"
:disabled="!isRunning"
:icon="faPowerOff"
>
{{ $t("shutdown") }}
</MenuItem>
<MenuItem
@click="xenApi.vm.shutdown({ vmRef: vm.$ref, force: true })"
:busy="isOperationsPending('hard_shutdown')"
:disabled="!isRunning && !isSuspended && !isPaused"
:icon="faPlug"
>
{{ $t("force-shutdown") }}
</MenuItem>
</AppMenu>
</template>
</TitleBar>
</template>
<script lang="ts" setup>
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import TitleBar from "@/components/TitleBar.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import { isHostRunning } from "@/libs/utils";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import {
faAngleDown,
faCirclePlay,
faDisplay,
faMoon,
faPause,
faPlay,
faPlug,
faPowerOff,
faRepeat,
faRotateLeft,
faServer,
faStar,
} from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import { useRouter } from "vue-router";
import { computedAsync } from "@vueuse/core";
import { difference } from "lodash";
const vmStore = useVmStore();
const hostStore = useHostStore();
const poolStore = usePoolStore();
const { currentRoute } = useRouter();
const isOperationsPending = (operations: string[] | string) => {
const _operations = Array.isArray(operations) ? operations : [operations];
return (
difference(_operations, vmOperations.value).length < _operations.length
);
};
const vmOperations = computed(() => Object.values(vm.value.current_operations));
const vm = computed(
() => vmStore.getRecordByUuid(currentRoute.value.params.uuid as string)!
);
const xenApi = computedAsync(() => useXenApiStore().getXapi());
const name = computed(() => vm.value.name_label);
const isRunning = computed(() => vm.value.power_state === "Running");
const isHalted = computed(() => vm.value.power_state === "Halted");
const isSuspended = computed(() => vm.value.power_state === "Suspended");
const isPaused = computed(() => vm.value.power_state === "Paused");
</script>
<style lang="postcss" scoped>
.star {
margin: 0 1rem;
color: var(--color-orange-world-base);
}
.wrapper {
display: flex;
justify-content: space-between;
width: 100%;
}
</style>

View File

@@ -1,9 +1,15 @@
<template>
<AppMenu
:disabled="selectedRefs.length === 0"
:horizontal="!isMobile"
:shadow="isMobile"
class="vms-actions-bar"
horizontal
placement="bottom-end"
>
<template v-if="isMobile" #trigger="{ isOpen, open }">
<UiButton :active="isOpen" :icon="faEllipsis" transparent @click="open" />
</template>
<MenuItem :icon="faPowerOff">{{ $t("change-power-state") }}</MenuItem>
<MenuItem :icon="faRoute">{{ $t("migrate") }}</MenuItem>
<MenuItem :icon="faCopy">{{ $t("copy") }}</MenuItem>
@@ -27,6 +33,10 @@
</template>
<script lang="ts" setup>
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useUiStore } from "@/stores/ui.store";
import {
faBox,
faCamera,
@@ -34,19 +44,21 @@ import {
faCopy,
faDisplay,
faEdit,
faEllipsis,
faFileCsv,
faFileExport,
faPowerOff,
faRoute,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons";
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import { storeToRefs } from "pinia";
defineProps<{
disabled?: boolean;
selectedRefs: string[];
}>();
const { isMobile } = storeToRefs(useUiStore());
</script>
<style lang="postcss" scoped>

View File

@@ -1,382 +1,411 @@
import { provide } from "vue";
import { useUiStore } from "@/stores/ui.store";
import { storeToRefs } from "pinia";
import { computed, provide, ref, watch } from "vue";
import { THEME_KEY } from "vue-echarts";
export const useChartTheme = () => {
provide(THEME_KEY, {
color: ["#8F84FF", "#EF7F18"],
backgroundColor: "#ffffff",
textStyle: {},
grid: {
top: 80,
left: 80,
right: 20,
},
title: {
textStyle: {
color: "#1A1B38",
fontFamily: "Poppins, sans-serif",
fontWeight: 500,
fontSize: 20,
const { colorMode } = storeToRefs(useUiStore());
const style = window.getComputedStyle(window.document.documentElement);
const getColors = () => ({
background: style.getPropertyValue("--background-color-primary"),
title: style.getPropertyValue("--color-blue-scale-100"),
subtitle: style.getPropertyValue("--color-blue-scale-300"),
splitLine: style.getPropertyValue("--color-blue-scale-400"),
primary: style.getPropertyValue("--color-extra-blue-base"),
secondary: style.getPropertyValue("--color-orange-world-base"),
});
const colors = ref(getColors());
watch(colorMode, () => (colors.value = getColors()), { flush: "post" });
provide(
THEME_KEY,
computed(() => ({
color: [colors.value.primary, colors.value.secondary],
backgroundColor: colors.value.background,
textStyle: {},
grid: {
top: 80,
left: 80,
right: 20,
},
subtextStyle: {
color: "#9899A5",
fontFamily: "Poppins, sans-serif",
fontWeight: 400,
fontSize: 14,
title: {
textStyle: {
color: colors.value.title,
fontFamily: "Poppins, sans-serif",
fontWeight: 500,
fontSize: 20,
},
subtextStyle: {
color: colors.value.subtitle,
fontFamily: "Poppins, sans-serif",
fontWeight: 400,
fontSize: 14,
},
},
},
line: {
itemStyle: {
borderWidth: 2,
},
lineStyle: {
width: 2,
},
showSymbol: false,
symbolSize: 10,
symbol: "circle",
smooth: false,
},
radar: {
itemStyle: {
borderWidth: 2,
},
lineStyle: {
width: 2,
},
symbolSize: 10,
symbol: "circle",
smooth: false,
},
bar: {
itemStyle: {
barBorderWidth: 0,
barBorderColor: "#cccccc",
},
},
pie: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
scatter: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
boxplot: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
parallel: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
sankey: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
funnel: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
gauge: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
candlestick: {
itemStyle: {
color: "#eb8146",
color0: "transparent",
borderColor: "#d95850",
borderColor0: "#58c470",
borderWidth: "2",
},
},
graph: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
lineStyle: {
width: 1,
color: "#aaaaaa",
},
symbolSize: "10",
symbol: "emptyArrow",
smooth: true,
color: ["#893448", "#d95850", "#eb8146", "#ffb248", "#f2d643", "#ebdba4"],
label: {
color: "#ffffff",
},
},
map: {
itemStyle: {
areaColor: "#f3f3f3",
borderColor: "#999999",
borderWidth: 0.5,
},
label: {
color: "#893448",
},
emphasis: {
line: {
itemStyle: {
areaColor: "#ffb248",
borderColor: "#eb8146",
borderWidth: 1,
borderWidth: 2,
},
lineStyle: {
width: 2,
},
showSymbol: false,
symbolSize: 10,
symbol: "circle",
smooth: false,
},
radar: {
itemStyle: {
borderWidth: 2,
},
lineStyle: {
width: 2,
},
symbolSize: 10,
symbol: "circle",
smooth: false,
},
bar: {
itemStyle: {
barBorderWidth: 0,
barBorderColor: "#cccccc",
},
},
pie: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
scatter: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
boxplot: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
parallel: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
sankey: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
funnel: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
gauge: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
},
candlestick: {
itemStyle: {
color: "#eb8146",
color0: "transparent",
borderColor: "#d95850",
borderColor0: "#58c470",
borderWidth: "2",
},
},
graph: {
itemStyle: {
borderWidth: 0,
borderColor: "#cccccc",
},
lineStyle: {
width: 1,
color: "#aaaaaa",
},
symbolSize: "10",
symbol: "emptyArrow",
smooth: true,
color: [
"#893448",
"#d95850",
"#eb8146",
"#ffb248",
"#f2d643",
"#ebdba4",
],
label: {
color: "#ffffff",
},
},
map: {
itemStyle: {
areaColor: "#f3f3f3",
borderColor: "#999999",
borderWidth: 0.5,
},
label: {
color: "#893448",
},
emphasis: {
itemStyle: {
areaColor: "#ffb248",
borderColor: "#eb8146",
borderWidth: 1,
},
label: {
color: "#893448",
},
},
},
},
geo: {
itemStyle: {
areaColor: "#f3f3f3",
borderColor: "#999999",
borderWidth: 0.5,
},
label: {
color: "#893448",
},
emphasis: {
geo: {
itemStyle: {
areaColor: "#ffb248",
borderColor: "#eb8146",
borderWidth: 1,
areaColor: "#f3f3f3",
borderColor: "#999999",
borderWidth: 0.5,
},
label: {
color: "#893448",
},
},
},
categoryAxis: {
axisLine: {
show: true,
lineStyle: {
color: "#aaaaaa",
emphasis: {
itemStyle: {
areaColor: "#ffb248",
borderColor: "#eb8146",
borderWidth: 1,
},
label: {
color: "#893448",
},
},
},
axisTick: {
show: false,
lineStyle: {
color: "#333",
categoryAxis: {
axisLine: {
show: true,
lineStyle: {
color: "#aaaaaa",
},
},
axisTick: {
show: false,
lineStyle: {
color: "#333",
},
},
axisLabel: {
show: true,
color: "#999999",
},
splitLine: {
show: true,
lineStyle: {
color: [colors.value.splitLine],
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
},
},
},
axisLabel: {
show: true,
color: "#999999",
},
splitLine: {
show: true,
lineStyle: {
color: ["#e6e6e6"],
valueAxis: {
axisLine: {
show: false,
// lineStyle: {
// color: "#aaaaaa",
// },
},
axisTick: {
show: false,
// lineStyle: {
// color: "#333",
// },
},
axisLabel: {
show: true,
color: colors.value.subtitle,
},
splitLine: {
show: true,
lineStyle: {
color: [colors.value.splitLine],
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
},
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
logAxis: {
axisLine: {
show: true,
lineStyle: {
color: "#aaaaaa",
},
},
axisTick: {
show: false,
lineStyle: {
color: "#333",
},
},
axisLabel: {
show: true,
color: "#999999",
},
splitLine: {
show: true,
lineStyle: {
color: [colors.value.splitLine],
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
},
},
},
},
valueAxis: {
axisLine: {
show: false,
// lineStyle: {
// color: "#aaaaaa",
// },
},
axisTick: {
show: false,
// lineStyle: {
// color: "#333",
// },
},
axisLabel: {
show: true,
color: "#9899A5",
},
splitLine: {
show: true,
lineStyle: {
color: ["#E5E5E7"],
timeAxis: {
axisLine: {
show: false,
// lineStyle: {
// color: "#aaaaaa",
// },
},
axisTick: {
show: false,
// lineStyle: {
// color: "#333",
// },
},
axisLabel: {
show: true,
color: colors.value.subtitle,
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: [colors.value.splitLine],
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
},
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
},
},
},
logAxis: {
axisLine: {
show: true,
lineStyle: {
color: "#aaaaaa",
},
},
axisTick: {
show: false,
lineStyle: {
color: "#333",
},
},
axisLabel: {
show: true,
color: "#999999",
},
splitLine: {
show: true,
lineStyle: {
color: ["#e6e6e6"],
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
},
},
},
timeAxis: {
axisLine: {
show: false,
// lineStyle: {
// color: "#aaaaaa",
// },
},
axisTick: {
show: false,
// lineStyle: {
// color: "#333",
// },
},
axisLabel: {
show: true,
color: "#9899A5",
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: ["#E5E5E7"],
},
},
splitArea: {
show: false,
areaStyle: {
color: ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"],
},
},
},
toolbox: {
iconStyle: {
borderColor: "#999999",
},
emphasis: {
toolbox: {
iconStyle: {
borderColor: "#666666",
borderColor: "#999999",
},
emphasis: {
iconStyle: {
borderColor: "#666666",
},
},
},
},
legend: {
left: "right",
top: "bottom",
textStyle: {
color: "#9899A5",
legend: {
left: "right",
top: "bottom",
textStyle: {
color: colors.value.subtitle,
},
},
},
tooltip: {
trigger: "axis",
axisPointer: {
tooltip: {
trigger: "axis",
axisPointer: {
lineStyle: {
color: "#8F84FF",
width: 1,
},
crossStyle: {
color: "#8F84FF",
width: 1,
},
},
},
timeline: {
lineStyle: {
color: "#8F84FF",
color: "#893448",
width: 1,
},
crossStyle: {
color: "#8F84FF",
width: 1,
},
},
},
timeline: {
lineStyle: {
color: "#893448",
width: 1,
},
itemStyle: {
color: "#893448",
borderWidth: 1,
},
controlStyle: {
color: "#893448",
borderColor: "#893448",
borderWidth: 0.5,
},
checkpointStyle: {
color: "#eb8146",
borderColor: "#ffb248",
},
label: {
color: "#893448",
},
emphasis: {
itemStyle: {
color: "#ffb248",
color: "#893448",
borderWidth: 1,
},
controlStyle: {
color: "#893448",
borderColor: "#893448",
borderWidth: 0.5,
},
checkpointStyle: {
color: "#eb8146",
borderColor: "#ffb248",
},
label: {
color: "#893448",
},
emphasis: {
itemStyle: {
color: "#ffb248",
},
controlStyle: {
color: "#893448",
borderColor: "#893448",
borderWidth: 0.5,
},
label: {
color: "#893448",
},
},
},
},
visualMap: {
color: [
"#893448",
"#d95850",
"#eb8146",
"#ffb248",
"#f2d643",
"rgb(247,238,173)",
],
},
dataZoom: {
backgroundColor: "rgba(255,255,255,0)",
dataBackgroundColor: "rgba(255,178,72,0.5)",
fillerColor: "rgba(255,178,72,0.15)",
handleColor: "#ffb248",
handleSize: "100%",
textStyle: {
color: "#333",
visualMap: {
color: [
"#893448",
"#d95850",
"#eb8146",
"#ffb248",
"#f2d643",
"rgb(247,238,173)",
],
},
},
markPoint: {
label: {
color: "#ffffff",
dataZoom: {
backgroundColor: "rgba(255,255,255,0)",
dataBackgroundColor: "rgba(255,178,72,0.5)",
fillerColor: "rgba(255,178,72,0.15)",
handleColor: "#ffb248",
handleSize: "100%",
textStyle: {
color: "#333",
},
},
emphasis: {
markPoint: {
label: {
color: "#ffffff",
},
emphasis: {
label: {
color: "#ffffff",
},
},
},
},
});
}))
);
};

View File

@@ -3,34 +3,37 @@
## Usage
```typescript
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
const { filters, addFilter, removeFilter, predicate } =
useCollectionFilter(options);
const filteredCollection = myCollection.filter(predicate);
const filteredCollection = computed(() => myCollection.filter(predicate));
addFilter("name:/^Foo/");
addFilter("count:>3");
```
## URL Query String
## Options
By default, when adding/removing filters, the URL will update automatically.
### `queryStringParam`
This option allows to activate the URL Query String support.
```typescript
const { addFilter } = useCollectionFilter({ queryStringParam: "filter" });
addFilter("name:/^foo/i"); // Will update the URL with ?filter=name:/^foo/i
```
### Change the URL query string parameter name
### Initial filters
This option allows to set some initial filters.
```typescript
const {
/* ... */
} = useCollectionFilter({ queryStringParam: "f" }); // ?f=name:/^foo/i
} = useCollectionFilter({ initialFilters: ["!name_label:foobar"] });
```
### Disable the usage of URL query string
```typescript
const {
/* ... */
} = useCollectionFilter({ queryStringParam: undefined });
```
When using the `initialFilters` option with the `queryStringParam` option,
`initialFilters` will only be applied if no query string parameter is defined in the URL.
## Example of using the composable with the `CollectionFilter` component

View File

@@ -1,25 +1,27 @@
import { getFirst } from "@/libs/utils";
import * as CM from "complex-matcher";
import { computed, ref, watch } from "vue";
import { type LocationQueryValue, useRoute, useRouter } from "vue-router";
interface Config {
queryStringParam?: string;
initialFilters?: string[];
}
export default function useCollectionFilter(
config: Config = { queryStringParam: "filter" }
) {
export default function useCollectionFilter<T>(config: Config = {}) {
const route = useRoute();
const router = useRouter();
const filtersSet = ref(
config.queryStringParam
? queryToSet(route.query[config.queryStringParam] as LocationQueryValue)
: new Set<string>()
);
const { queryStringParam, initialFilters = [] } = config;
const filtersSet = ref<Set<string>>(new Set(initialFilters));
const filters = computed(() => Array.from(filtersSet.value.values()));
if (config.queryStringParam) {
const queryStringParam = config.queryStringParam;
if (queryStringParam !== undefined) {
const queryString = route.query[queryStringParam];
if (queryString !== undefined) {
filtersSet.value = queryToSet(getFirst(queryString));
}
watch(filters, (value) =>
router.replace({
query: { ...route.query, [queryStringParam]: value.join(" ") },
@@ -35,7 +37,7 @@ export default function useCollectionFilter(
filtersSet.value.delete(filter);
};
const predicate = computed(() => {
const predicate = computed<(value: T) => boolean>(() => {
return CM.parse(
Array.from(filters.value.values()).join(" ")
).createPredicate();
@@ -49,7 +51,7 @@ export default function useCollectionFilter(
};
}
function queryToSet(query: LocationQueryValue): Set<string> {
function queryToSet(query?: LocationQueryValue): Set<string> {
if (!query) {
return new Set();
}

View File

@@ -0,0 +1,40 @@
# useCollectionSorter composable
## Usage
```typescript
const { sorts, addSort, removeSort, compareFn, toggleSortDirection } =
useCollectionSorter(options);
const sortedCollection = computed(() => myCollection.sort(compareFn));
addSort("name", true);
addSort("age", false);
```
## Options
### `queryStringParam`
This option allows to activate the URL Query String support.
```typescript
const { addSort } = useCollectionSorter({ queryStringParam: "sort" });
addSort("name", true); // Will update the URL with ?sort=name:1
```
### Initial sorts
This option allows to set some initial sorts.
Use `key` for ascending sort and `-key` for descending sort.
```typescript
const {
/* ... */
} = useCollectionSorter({
initialSorts: ["name", "-age"],
});
```
When using the `initialSorts` option with the `queryStringParam` option,
`initialSorts` will only be applied if no query string parameter is defined in the URL.

View File

@@ -1,52 +1,43 @@
import { getFirst } from "@/libs/utils";
import type { ActiveSorts, InitialSorts, SortConfig } from "@/types/sort";
import { computed, ref, watch } from "vue";
import { type LocationQueryValue, useRoute, useRouter } from "vue-router";
import type { ActiveSorts } from "@/types/sort";
interface Config {
queryStringParam?: string;
}
export default function useCollectionSorter(
config: Config = { queryStringParam: "sort" }
) {
export default function useCollectionSorter<T>(config: SortConfig<T> = {}) {
const route = useRoute();
const router = useRouter();
const { queryStringParam, initialSorts = [] } = config;
const sorts = ref<ActiveSorts<T>>(parseInitialSorts(initialSorts));
const sorts = ref<ActiveSorts>(
config.queryStringParam
? queryToMap(route.query[config.queryStringParam] as LocationQueryValue)
: new Map()
const sortsAsString = computed(() =>
Array.from(sorts.value)
.map(([property, isAsc]) => `${String(property)}:${isAsc ? "1" : "0"}`)
.join(",")
);
if (config.queryStringParam) {
const queryStringParam = config.queryStringParam;
watch(
sorts,
(value) =>
router.replace({
query: {
...route.query,
[queryStringParam]: Array.from(value)
.map(
([property, isAscending]) =>
`${property}:${isAscending ? "1" : "0"}`
)
.join(","),
},
}),
{ deep: true }
if (queryStringParam !== undefined) {
const queryString = route.query[queryStringParam];
if (queryString !== undefined) {
sorts.value = queryToMap(getFirst(queryString));
}
watch(sortsAsString, (value) =>
router.replace({
query: { ...route.query, [queryStringParam]: value },
})
);
}
const addSort = (property: string, isAscending: boolean) => {
const addSort = (property: keyof T, isAscending: boolean) => {
sorts.value.set(property, isAscending);
};
const removeSort = (property: string) => {
const removeSort = (property: keyof T) => {
sorts.value.delete(property);
};
const toggleSortDirection = (property: string) => {
const toggleSortDirection = (property: keyof T) => {
if (!sorts.value.has(property)) {
return;
}
@@ -55,7 +46,7 @@ export default function useCollectionSorter(
};
const compareFn = computed(() => {
return (record1: any, record2: any) => {
return (record1: T, record2: T) => {
for (const [property, isAscending] of sorts.value) {
const value1 = record1[property];
const value2 = record2[property];
@@ -82,7 +73,7 @@ export default function useCollectionSorter(
};
}
function queryToMap(query: LocationQueryValue) {
function queryToMap(query?: LocationQueryValue) {
if (!query) {
return new Map();
}
@@ -94,3 +85,14 @@ function queryToMap(query: LocationQueryValue) {
})
);
}
function parseInitialSorts<T>(sorts: InitialSorts<T>): ActiveSorts<T> {
return new Map(
sorts.map((sort) => {
const isDescending = sort.startsWith("-");
const property = (isDescending ? sort.substring(1) : sort) as keyof T;
return [property, !isDescending];
})
);
}

View File

@@ -1,6 +1,12 @@
import { computed, onUnmounted, ref } from "vue";
import { computed, onUnmounted, ref, type ComputedRef } from "vue";
import { type Pausable, promiseTimeout, useTimeoutPoll } from "@vueuse/core";
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
import {
type GRANULARITY,
type HostStats,
RRD_STEP_FROM_STRING,
type VmStats,
type XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
@@ -17,39 +23,57 @@ export type Stat<T> = {
pausable: Pausable;
};
export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
type: "host" | "vm",
granularity: GRANULARITY
) {
export type FetchedStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats
> = {
register: (object: T) => void;
unregister: (object: T) => void;
stats?: ComputedRef<Stat<S>[]>;
timestampStart?: ComputedRef<number>;
timestampEnd?: ComputedRef<number>;
};
export default function useFetchStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats
>(type: "host" | "vm", granularity: GRANULARITY) {
const stats = ref<Map<string, Stat<S>>>(new Map());
const timestamp = ref<number[]>([0, 0]);
const register = (object: T) => {
if (stats.value.has(object.uuid)) {
stats.value.get(object.uuid)!.pausable.resume();
const mapKey = `${object.uuid}-${granularity}`;
if (stats.value.has(mapKey)) {
stats.value.get(mapKey)!.pausable.resume();
return;
}
const pausable = useTimeoutPoll(
async () => {
if (!stats.value.has(object.uuid)) {
if (!stats.value.has(mapKey)) {
return;
}
const newStats = (await STORES_BY_OBJECT_TYPE[type]().getStats(
object.uuid,
granularity
)) as XapiStatsResponse<S> | undefined;
)) as XapiStatsResponse<S>;
if (newStats !== undefined) {
stats.value.get(object.uuid)!.stats = newStats.stats;
await promiseTimeout(newStats.interval * 1000);
}
timestamp.value = [
newStats.endTimestamp -
RRD_STEP_FROM_STRING[granularity] *
(newStats.stats.memory.length - 1),
newStats.endTimestamp,
];
stats.value.get(mapKey)!.stats = newStats.stats;
await promiseTimeout(newStats.interval * 1000);
},
0,
{ immediate: true }
);
stats.value.set(object.uuid, {
stats.value.set(mapKey, {
id: object.uuid,
name: object.name_label,
stats: undefined,
@@ -58,8 +82,9 @@ export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
};
const unregister = (object: T) => {
stats.value.get(object.uuid)?.pausable.pause();
stats.value.delete(object.uuid);
const mapKey = `${object.uuid}-${granularity}`;
stats.value.get(mapKey)?.pausable.pause();
stats.value.delete(mapKey);
};
onUnmounted(() => {
@@ -70,5 +95,7 @@ export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
register,
unregister,
stats: computed<Stat<S>[]>(() => Array.from(stats.value.values())),
timestampStart: computed(() => timestamp.value[0]),
timestampEnd: computed(() => timestamp.value[1]),
};
}

View File

@@ -0,0 +1,18 @@
# useRelativeTime composable
## Usage
```ts
const relativeTime = useRelativeTime(fromDate, toDate);
console.log(relativeTime.value); // 3 days 27 minutes 10 seconds ago
```
# Reactivity
Both arguments can be `Ref`
```ts
const now = useNow();
const relativeTime = useRelativeTime(fromDate, now); // Value will be updated each time `now` changes
```

View File

@@ -0,0 +1,66 @@
import type { MaybeRef } from "@vueuse/core";
import { computed, unref } from "vue";
import { useI18n } from "vue-i18n";
export default function useRelativeTime(
fromDate: MaybeRef<Date>,
toDate: MaybeRef<Date>
) {
const { t } = useI18n();
const fromTime = computed(() => unref(fromDate).getTime());
const toTime = computed(() => unref(toDate).getTime());
const isPast = computed(() => toTime.value > fromTime.value);
const diff = computed(() => Math.abs(toTime.value - fromTime.value));
return computed(() => {
if (diff.value < 10000) {
return t("relative-time.now");
}
const years = Math.floor(diff.value / 31556952000);
let timeLeft = diff.value % 31556952000;
const months = Math.floor(timeLeft / 2629746000);
timeLeft = timeLeft % 2629746000;
const days = Math.floor(timeLeft / 86400000);
timeLeft = timeLeft % 86400000;
const hours = Math.floor(timeLeft / 3600000);
timeLeft = timeLeft % 3600000;
const minutes = Math.floor(timeLeft / 60000);
timeLeft = timeLeft % 60000;
const seconds = Math.floor(timeLeft / 1000);
const parts = [];
if (years > 0) {
parts.push(t("relative-time.year", { n: years }));
}
if (months > 0) {
parts.push(t("relative-time.month", { n: months }));
}
if (days > 0) {
parts.push(t("relative-time.day", { n: days }));
}
if (years === 0 && months === 0 && days <= 1 && hours > 0) {
parts.push(t("relative-time.hour", { n: hours }));
}
if (years === 0 && months === 0 && days === 0 && minutes > 0) {
parts.push(t("relative-time.minute", { n: minutes }));
}
if (years === 0 && months === 0 && days === 0 && seconds > 0) {
parts.push(t("relative-time.second", { n: seconds }));
}
return t(isPast.value ? "relative-time.past" : "relative-time.future", {
str: parts.join(" "),
});
});
}

View File

@@ -116,6 +116,20 @@ export function isHostRunning(host: XenApiHost) {
}
}
export function getHostMemory(host: XenApiHost) {
try {
const metrics = useHostMetricsStore().getRecord(host.metrics);
const total = +metrics.memory_total;
return {
usage: total - +metrics.memory_free,
size: total,
};
} catch (error) {
console.error("getHostMemory function:", error);
return undefined;
}
}
export const buildXoObject = (
record: RawXenApiRecord<XenApiRecord>,
params: { opaqueRef: string }
@@ -157,3 +171,6 @@ export function parseRamUsage(
used: memoryFree === undefined ? 0 : used / _nSequence,
};
}
export const getFirst = <T>(value: T | T[]): T | undefined =>
Array.isArray(value) ? value[0] : value;

View File

@@ -30,7 +30,7 @@ export enum GRANULARITY {
Days = "days",
}
const RRD_STEP_FROM_STRING: { [key in GRANULARITY]: RRD_STEP } = {
export const RRD_STEP_FROM_STRING: { [key in GRANULARITY]: RRD_STEP } = {
[GRANULARITY.Seconds]: RRD_STEP.Seconds,
[GRANULARITY.Minutes]: RRD_STEP.Minutes,
[GRANULARITY.Hours]: RRD_STEP.Hours,
@@ -365,7 +365,7 @@ export default class XapiStats {
`Unknown granularity: '${granularity}'. Use 'seconds', 'minutes', 'hours', or 'days'.`
);
}
const currentTimeStamp = await this.#xapi.getHostServertime(host);
const currentTimeStamp = Math.floor(new Date().getTime() / 1000);
const stats = this.#getCachedStats(uuid, step, currentTimeStamp);
if (stats !== undefined) {

View File

@@ -1,5 +1,6 @@
import { JSONRPCClient } from "json-rpc-2.0";
import { buildXoObject, parseDateTime } from "@/libs/utils";
import { useVmStore } from "@/stores/vm.store";
export type RawObjectType =
| "Bond"
@@ -69,6 +70,7 @@ export interface XenApiRecord {
export type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">;
export interface XenApiPool extends XenApiRecord {
master: string;
name_label: string;
}
@@ -77,6 +79,7 @@ export interface XenApiHost extends XenApiRecord {
name_label: string;
metrics: string;
resident_VMs: string[];
cpu_info: { cpu_count: string };
}
export interface XenApiSr extends XenApiRecord {
@@ -86,6 +89,7 @@ export interface XenApiSr extends XenApiRecord {
}
export interface XenApiVm extends XenApiRecord {
current_operations: Record<string, string>;
name_label: string;
name_description: string;
power_state: PowerState;
@@ -199,6 +203,9 @@ export default class XenApi {
return this.#client.request(method, args);
}
_call = (method: string, args: any[] = []) =>
this.#call(method, [this.sessionId, ...args]);
async getHostServertime(host: XenApiHost) {
const serverLocaltime = (await this.#call("host.get_servertime", [
this.sessionId,
@@ -281,4 +288,83 @@ export default class XenApi {
poolRef,
]);
}
get vm() {
type VmsRef =
| {
vmRef: XenApiVm["$ref"];
vmsRef?: undefined;
}
| {
vmRef?: undefined;
vmsRef: XenApiVm["$ref"][];
};
return {
start: ({ vmRef, vmsRef }: VmsRef) => {
const _vmsRef = vmsRef ?? [vmRef];
return Promise.all(
_vmsRef.map((vmRef) => this._call("VM.start", [vmRef, false, false]))
);
},
startOn: ({ vmRef, vmsRef, hostRef }: VmsRef & { hostRef: string }) => {
const _vmsRef = vmsRef ?? [vmRef];
return Promise.all(
_vmsRef.map((vmRef) =>
this._call("VM.start_on", [vmRef, hostRef, false, false])
)
);
},
pause: ({ vmRef, vmsRef }: VmsRef) => {
const _vmsRef = vmsRef ?? [vmRef];
return Promise.all(
_vmsRef.map((vmRef) => this._call("VM.pause", [vmRef]))
);
},
suspend: ({ vmRef, vmsRef }: VmsRef) => {
const _vmsRef = vmsRef ?? [vmRef];
return Promise.all(
_vmsRef.map((vmRef) => this._call("VM.suspend", [vmRef]))
);
},
resume: ({ vmRef, vmsRef }: VmsRef) => {
const _vmsRef = vmsRef ?? [vmRef];
const vmStore = useVmStore();
return Promise.all(
_vmsRef.map((ref) => {
const isSuspended =
vmStore.getRecord(ref).power_state === "Suspended";
return this._call(
`VM.${isSuspended ? "resume" : "unpause"}`,
isSuspended ? [ref, false, false] : [ref]
);
})
);
},
reboot: ({
vmRef,
vmsRef,
force = false,
}: VmsRef & { force?: boolean }) => {
const _vmsRef = vmsRef ?? [vmRef];
return Promise.all(
_vmsRef.map((vmRef) =>
this._call(`VM.${force ? "hard" : "clean"}_reboot`, [vmRef])
)
);
},
shutdown: ({
vmRef,
vmsRef,
force = false,
}: VmsRef & { force?: boolean }) => {
const _vmsRef = vmsRef ?? [vmRef];
return Promise.all(
_vmsRef.map((vmRef) =>
this._call(`VM.${force ? "hard" : "clean"}_shutdown`, [vmRef])
)
);
},
};
}
}

View File

@@ -4,21 +4,19 @@
"add-or": "+OR",
"add-sort": "Add sort",
"alarms": "Alarms",
"allow-self-signed-ssl":"You may need to allow self-signed SSL certificates in your browser",
"allow-self-signed-ssl": "You may need to allow self-signed SSL certificates in your browser",
"appearance": "Appearance",
"ascending": "ascending",
"available-properties-for-advanced-filter": "Available properties for advanced filter:",
"backup": "Backup",
"back-pool-dashboard": "Go back to your Pool dashboard",
"backup": "Backup",
"cancel": "Cancel",
"change-power-state": "Change power state",
"change-state": "Change state",
"community": "Community",
"community-name": "{name} community",
"copy": "Copy",
"cpu-usage":"CPU usage",
"theme-dark": "Dark",
"theme-light": "Light",
"theme-auto": "Auto",
"cpu-usage": "CPU usage",
"dashboard": "Dashboard",
"delete": "Delete",
"descending": "descending",
@@ -28,14 +26,20 @@
"export": "Export",
"export-table-to": "Export table to {type}",
"export-vms": "Export VMs",
"following-hosts-unreachable": "The following hosts are unreachable",
"force-reboot": "Force reboot",
"force-shutdown": "Force shutdown",
"hosts": "Hosts",
"language": "Language",
"following-hosts-unreachable":"The following hosts are unreachable",
"last-week": "Last week",
"loading-hosts": "Loading hosts…",
"log-out": "Log out",
"login": "Login",
"migrate": "Migrate",
"network": "Network",
"network-download": "Download",
"network-throughput": "Network throughput",
"network-upload": "Upload",
"news": "News",
"news-name": "{name} news",
"object-not-found": "Object {id} can't be found…",
@@ -43,19 +47,44 @@
"page-not-found": "This page is not to be found…",
"password": "Password",
"password-invalid": "Password invalid",
"pause": "Pause",
"pool-cpu-usage": "Pool CPU Usage",
"pool-ram-usage": "Pool RAM Usage",
"property": "Property",
"ram-usage":"RAM usage",
"ram-usage": "RAM usage",
"reboot": "Reboot",
"relative-time": {
"day": "1 day | {n} days",
"future": "In {str}",
"hour": "1 hour | {n} hours",
"minute": "1 minute | {n} minutes",
"month": "1 month | {n} months",
"now": "Just now",
"past": "{str} ago",
"second": "1 second | {n} seconds",
"year": "1 year | {n} years"
},
"resume": "Resume",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"shutdown": "Shutdown",
"snapshot": "Snapshot",
"sort-by": "Sort by",
"stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage",
"start": "Start",
"start-on-host": "Start on specific host",
"stats": "Stats",
"status": "Status",
"storage": "Storage",
"storage-usage": "Storage usage",
"suspend": "Suspend",
"switch-theme": "Switch theme",
"system": "System",
"tasks": "Tasks",
"theme-auto": "Auto",
"theme-dark": "Dark",
"theme-light": "Light",
"top-#": "Top {n}",
"total-free": "Total free",
"total-used": "Total used",

View File

@@ -4,21 +4,19 @@
"add-or": "+OU",
"add-sort": "Ajouter un tri",
"alarms": "Alarmes",
"allow-self-signed-ssl": "Vous devrez peut-être autoriser les certificats SSL auto-signés depuis votre navigateur",
"appearance": "Apparence",
"allow-self-signed-ssl":"Vous devrez peut-être autoriser les certificats SSL auto-signés depuis votre navigateur",
"ascending": "ascendant",
"available-properties-for-advanced-filter": "Propriétés disponibles pour le filtrage avancé :",
"backup": "Sauvegarde",
"back-pool-dashboard": "Revenez au tableau de bord de votre pool",
"backup": "Sauvegarde",
"cancel": "Annuler",
"change-power-state": "Changer l'état d'alimentation",
"change-state": "Changer l'état",
"community": "Communauté",
"community-name": "Communauté {name}",
"copy": "Copier",
"cpu-usage":"Utilisation CPU",
"theme-dark": "Sombre",
"theme-light": "Clair",
"theme-auto": "Auto",
"cpu-usage": "Utilisation CPU",
"dashboard": "Tableau de bord",
"delete": "Supprimer",
"descending": "descendant",
@@ -28,14 +26,20 @@
"export": "Exporter",
"export-table-to": "Exporter le tableau en {type}",
"export-vms": "Exporter les VMs",
"following-hosts-unreachable": "Les hôtes suivants sont inaccessibles",
"force-reboot": "Forcer le redémarrage",
"force-shutdown": "Forcer l'arrêt",
"hosts": "Hôtes",
"language": "Langue",
"following-hosts-unreachable":"Les hôtes suivants sont inaccessibles",
"last-week": "Semaine dernière",
"loading-hosts": "Chargement des hôtes…",
"log-out": "Se déconnecter",
"login": "Connexion",
"migrate": "Migrer",
"network": "Réseau",
"network-download": "Descendant",
"network-throughput": "Débit du réseau",
"network-upload": "Montant",
"news": "Actualités",
"news-name": "Actualités {name}",
"object-not-found": "L'objet {id} est introuvable…",
@@ -43,19 +47,44 @@
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"pause": "Pause",
"pool-cpu-usage": "Utilisation CPU du Pool",
"pool-ram-usage": "Utilisation RAM du Pool",
"property": "Propriété",
"ram-usage":"Utilisation de la RAM",
"ram-usage": "Utilisation de la RAM",
"reboot": "Redémarrer",
"relative-time": {
"day": "1 jour | {n} jours",
"future": "Dans {str}",
"hour": "1 heure | {n} heures",
"minute": "1 minute | {n} minutes",
"month": "{n} mois",
"now": "À l'instant",
"past": "Il y a {str}",
"second": "1 seconde | {n} secondes",
"year": "1 an | {n} ans"
},
"resume": "Reprendre",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"shutdown": "Arrêter",
"snapshot": "Instantané",
"sort-by": "Trier par",
"stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée",
"start": "Démarrer",
"start-on-host": "Démarrer sur un hôte spécifique",
"stats": "Stats",
"status": "Statut",
"storage": "Stockage",
"storage-usage": "Utilisation du stockage",
"suspend": "Suspendre",
"switch-theme": "Changer de thème",
"system": "Système",
"tasks": "Tâches",
"theme-auto": "Auto",
"theme-dark": "Sombre",
"theme-light": "Clair",
"top-#": "Top {n}",
"total-free": "Total libre",
"total-used": "Total utilisé",

View File

@@ -0,0 +1,32 @@
import { useUiStore } from "@/stores/ui.store";
import { useEventListener, whenever } from "@vueuse/core";
import { defineStore } from "pinia";
import { ref } from "vue";
import { useRouter } from "vue-router";
export const useNavigationStore = defineStore("navigation", () => {
const router = useRouter();
const uiStore = useUiStore();
const trigger = ref();
const isOpen = ref(false);
const toggle = () => (isOpen.value = !isOpen.value);
// Close the menu when the user navigates to a new page
router.afterEach(() => {
isOpen.value = false;
});
useEventListener(trigger, "click", toggle);
whenever(
() => uiStore.isDesktop,
() => (isOpen.value = false)
);
return {
trigger,
toggle,
isOpen,
};
});

View File

@@ -1,14 +1,22 @@
import { useColorMode } from "@vueuse/core";
import { useBreakpoints, useColorMode } from "@vueuse/core";
import { defineStore } from "pinia";
import { ref } from "vue";
import { computed, ref } from "vue";
export const useUiStore = defineStore("ui", () => {
const currentHostOpaqueRef = ref();
const colorMode = useColorMode({ emitAuto: true, initialValue: "dark" });
const { desktop: isDesktop } = useBreakpoints({
desktop: 1024,
});
const isMobile = computed(() => !isDesktop.value);
return {
colorMode,
currentHostOpaqueRef,
isDesktop,
isMobile,
};
});

View File

@@ -1,7 +1,7 @@
export type LinearChartData = {
label: string;
data: {
date: string;
timestamp: number;
value: number;
}[];
}[];

View File

@@ -5,6 +5,15 @@ interface Sort {
icon?: IconDefinition;
}
export type Sorts = { [key: string]: Sort };
export interface Sorts {
[key: string]: Sort;
}
export type ActiveSorts = Map<string, boolean>;
export type ActiveSorts<T> = Map<keyof T, boolean>;
export type InitialSorts<T> = `${"-" | ""}${Extract<keyof T, string>}`[];
export interface SortConfig<T> {
queryStringParam?: string;
initialSorts?: InitialSorts<T>;
}

View File

@@ -1,9 +1,26 @@
<template>
<div class="pool-dashboard-view card-view">
<PoolDashboardStatus class="item" />
<PoolDashboardStorageUsage class="item" />
<PoolDashboardCpuUsage class="item" />
<PoolDashboardRamUsage class="item" />
<div class="pool-dashboard-view">
<div class="item">
<PoolDashboardStatus />
</div>
<div class="item">
<PoolDashboardStorageUsage />
</div>
<div class="item">
<PoolDashboardCpuUsage />
</div>
<div class="item">
<PoolDashboardRamUsage />
</div>
<div class="item">
<PoolDashboardNetworkChart />
</div>
<div class="item">
<PoolDashboardRamUsageChart />
</div>
<div class="item">
<PoolCpuUsageChart />
</div>
</div>
</template>
@@ -13,8 +30,11 @@ export const N_ITEMS = 5;
<script lang="ts" setup>
import { differenceBy } from "lodash-es";
import { computed, onMounted, provide, watch } from "vue";
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import useFetchStats from "@/composables/fetch-stats.composable";
@@ -38,6 +58,11 @@ const {
stats: vmStats,
} = useFetchStats<XenApiVm, VmStats>("vm", GRANULARITY.Seconds);
const hostLastWeekStats = useFetchStats<XenApiHost, HostStats>(
"host",
GRANULARITY.Hours
);
const runningHosts = computed(() => hostStore.allRecords.filter(isHostRunning));
const runningVms = computed(() =>
vmStore.allRecords.filter((vm) => vm.power_state === "Running")
@@ -46,30 +71,67 @@ const runningVms = computed(() =>
provide("hostStats", hostStats);
provide("vmStats", vmStats);
provide("hostLastWeekStats", hostLastWeekStats);
watch(runningHosts, (hosts, previousHosts) => {
// turned On
differenceBy(hosts, previousHosts ?? [], "uuid").forEach(hostRegister);
differenceBy(hosts, previousHosts ?? [], "uuid").forEach((host) => {
hostRegister(host);
hostLastWeekStats.register(host);
});
// turned Off
differenceBy(previousHosts, hosts, "uuid").forEach(hostUnregister);
differenceBy(previousHosts, hosts, "uuid").forEach((host) => {
hostUnregister(host);
hostLastWeekStats.unregister(host);
});
});
watch(runningVms, (vms, previousVms) => {
// turned On
differenceBy(vms, previousVms ?? [], "uuid").forEach(vmRegister);
differenceBy(vms, previousVms ?? [], "uuid").forEach((vm) => vmRegister(vm));
// turned Off
differenceBy(previousVms, vms, "uuid").forEach(vmUnregister);
differenceBy(previousVms, vms, "uuid").forEach((vm) => vmUnregister(vm));
});
onMounted(() => {
runningHosts.value.forEach(hostRegister);
runningVms.value.forEach(vmRegister);
runningHosts.value.forEach((host) => {
hostRegister(host);
hostLastWeekStats.register(host);
});
runningVms.value.forEach((vm) => vmRegister(vm));
});
</script>
<style lang="postcss" scoped>
.pool-dashboard-view {
column-gap: 0;
position: relative;
}
@media (min-width: 768px) {
.pool-dashboard-view {
column-count: 2;
}
}
@media (min-width: 1500px) {
.pool-dashboard-view {
column-count: 3;
}
}
.item {
min-width: 37rem;
margin: 0;
padding: 0.5rem;
}
@media (min-width: 768px) {
.item {
page-break-inside: avoid;
break-inside: avoid;
}
}
</style>

View File

@@ -2,9 +2,8 @@
<div class="pool-root-view">
<PoolHeader />
<PoolTabBar />
<div class="card-view">
<RouterView />
</div>
<RouterView />
</div>
</template>

View File

@@ -1,6 +1,12 @@
<template>
<UiCard class="pool-vms-view">
<VmsActionsBar :selected-refs="selectedVmsRefs" />
<UiCardTitle>
VMs
<template v-if="isMobile" #right>
<VmsActionsBar :selected-refs="selectedVmsRefs" />
</template>
</UiCardTitle>
<VmsActionsBar v-if="isDesktop" :selected-refs="selectedVmsRefs" />
<CollectionTable
v-model="selectedVmsRefs"
:available-filters="filters"
@@ -25,19 +31,21 @@
</template>
<script lang="ts" setup>
import { storeToRefs } from "pinia";
import { ref } from "vue";
import type { Filters } from "@/types/filter";
import { faPowerOff } from "@fortawesome/free-solid-svg-icons";
import CollectionTable from "@/components/CollectionTable.vue";
import ColumnHeader from "@/components/ColumnHeader.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import type { Filters } from "@/types/filter";
import { faPowerOff } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
import { ref } from "vue";
const vmStore = useVmStore();
const { allRecords: vms } = storeToRefs(vmStore);
const { allRecords: vms } = storeToRefs(useVmStore());
const { isMobile, isDesktop } = storeToRefs(useUiStore());
const filters: Filters = {
name_label: { label: "VM Name", type: "string" },
@@ -53,4 +61,8 @@ const filters: Filters = {
const selectedVmsRefs = ref([]);
</script>
<style lang="postcss" scoped></style>
<style lang="postcss" scoped>
.pool-vms-view {
overflow: auto;
}
</style>

View File

@@ -15,7 +15,7 @@ const route = useRoute();
const vmStore = useVmStore();
const consoleStore = useConsoleStore();
const isReady = computed(() => vmStore.isReady || consoleStore.isReady);
const isReady = computed(() => vmStore.isReady && consoleStore.isReady);
const vm = computed(() => vmStore.getRecordByUuid(route.params.uuid as string));
const isVmRunning = computed(() => vm.value?.power_state === "Running");

View File

@@ -1,11 +1,13 @@
<template>
<ObjectNotFoundWrapper object-type="vm">
<VmHeader />
<RouterView />
</ObjectNotFoundWrapper>
</template>
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import VmHeader from "@/components/vm/VmHeader.vue";
import { watchEffect } from "vue";
import { useRoute } from "vue-router";
import { useUiStore } from "@/stores/ui.store";

View File

@@ -3,10 +3,7 @@
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"experimentalDecorators": true,
"lib": [
"ES2019",
"dom"
],
"lib": ["ES2019", "ES2020.Intl", "dom"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]

View File

@@ -25,8 +25,12 @@ export default defineConfig({
commonjsOptions: {
include: [/complex-matcher/, /node_modules/],
},
target: "es2020",
},
optimizeDeps: {
include: ["complex-matcher"],
esbuildOptions: {
target: "es2020",
},
},
});

View File

@@ -141,3 +141,44 @@ configure(transportSyslog())
// But TCP, a different host, or a different port can be used
configure(transportSyslog({ target: 'tcp://syslog.company.lan' }))
```
### Helpers
#### Dedupe
> Wraps a transport to limit the number of duplicate logs.
```js
import { dedupe } from '@xen-orchestra/log/dedupe'
configure(
dedupe({
timeout: 500e3, // default to 600e3, ie 10 minutes
transport: console.log,
})
)
```
Duplicate logs will be buffered for `timeout` milliseconds or until a different log is emitted, at which time a dedicated log entry is emitted which indicate the number of duplicates that occured.
```js
const logger = createLogger('app')
// Log some duplicate messages
logger.error('Something went wrong')
logger.error('Something went wrong')
logger.error('Something went wrong')
// Log a different message
logger.info('This is a different message')
```
In this example, the first three log entries are identical and the last two will be treated as duplicates. They will be grouped together and sent to the transport as a single log with `nDuplicates: 2` data when a different log entry is emitted (or after the timeout as elasped if there weren't one).
The output in the console would look something like:
```
app ERROR Something went wrong
app ERROR duplicates of the previous log were hidden { nDuplicates: 2 }
app INFO This is a different message
```

View File

@@ -10,8 +10,8 @@
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/log):
```
> npm install --save @xen-orchestra/log
```sh
npm install --save @xen-orchestra/log
```
## Usage
@@ -160,6 +160,47 @@ configure(transportSyslog())
configure(transportSyslog({ target: 'tcp://syslog.company.lan' }))
```
### Helpers
#### Dedupe
> Wraps a transport to limit the number of duplicate logs.
```js
import { dedupe } from '@xen-orchestra/log/dedupe'
configure(
dedupe({
timeout: 500e3, // default to 600e3, ie 10 minutes
transport: console.log,
})
)
```
Duplicate logs will be buffered for `timeout` milliseconds or until a different log is emitted, at which time a dedicated log entry is emitted which indicate the number of duplicates that occured.
```js
const logger = createLogger('app')
// Log some duplicate messages
logger.error('Something went wrong')
logger.error('Something went wrong')
logger.error('Something went wrong')
// Log a different message
logger.info('This is a different message')
```
In this example, the first three log entries are identical and the last two will be treated as duplicates. They will be grouped together and sent to the transport as a single log with `nDuplicates: 2` data when a different log entry is emitted (or after the timeout as elasped if there weren't one).
The output in the console would look something like:
```
app ERROR Something went wrong
app ERROR duplicates of the previous log were hidden { nDuplicates: 2 }
app INFO This is a different message
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -0,0 +1,9 @@
'use strict'
module.exports = function Log(data, level, namespace, message) {
this.data = data
this.level = level
this.namespace = namespace
this.message = message
this.time = new Date()
}

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