Compare commits
10 Commits
fix-refs-b
...
gab-docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
124fd97d38 | ||
|
|
7037b15cb3 | ||
|
|
9253718d5c | ||
|
|
99f2b42243 | ||
|
|
43104d908c | ||
|
|
fde65bcd6a | ||
|
|
26711456d5 | ||
|
|
73a0356e4f | ||
|
|
a4a828a71c | ||
|
|
3d9b9efc04 |
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/async-each):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/async-each
|
||||
```
|
||||
> npm install --save @vates/async-each
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/cached-dns.lookup):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/cached-dns.lookup
|
||||
```
|
||||
> npm install --save @vates/cached-dns.lookup
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/coalesce-calls
|
||||
```
|
||||
> npm install --save @vates/coalesce-calls
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/compose):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/compose
|
||||
```
|
||||
> npm install --save @vates/compose
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/decorate-with
|
||||
```
|
||||
> npm install --save @vates/decorate-with
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/disposable):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/disposable
|
||||
```
|
||||
> npm install --save @vates/disposable
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/event-listeners-manager):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/event-listeners-manager
|
||||
```
|
||||
> npm install --save @vates/event-listeners-manager
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/multi-key-map):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/multi-key-map
|
||||
```
|
||||
> npm install --save @vates/multi-key-map
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/nbd-client):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/nbd-client
|
||||
```
|
||||
> npm install --save @vates/nbd-client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -16,7 +16,6 @@ const {
|
||||
NBD_REPLY_MAGIC,
|
||||
NBD_REQUEST_MAGIC,
|
||||
OPTS_MAGIC,
|
||||
NBD_CMD_DISC,
|
||||
} = require('./constants.js')
|
||||
const { fromCallback } = require('promise-toolbox')
|
||||
const { readChunkStrict } = require('@vates/read-chunk')
|
||||
@@ -90,11 +89,6 @@ module.exports = class NbdClient {
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
const buffer = Buffer.alloc(28)
|
||||
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
|
||||
buffer.writeInt16BE(0, 4) // no command flags for a disconnect
|
||||
buffer.writeInt16BE(NBD_CMD_DISC, 6) // we want to disconnect from nbd server
|
||||
await this.#write(buffer)
|
||||
await this.#serverSocket.destroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^1.2.5"
|
||||
"xen-api": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/otp):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/otp
|
||||
```
|
||||
> npm install --save @vates/otp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/parse-duration):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/parse-duration
|
||||
```
|
||||
> npm install --save @vates/parse-duration
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/predicates):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/predicates
|
||||
```
|
||||
> npm install --save @vates/predicates
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/read-chunk):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/read-chunk
|
||||
```
|
||||
> npm install --save @vates/read-chunk
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Read a chunk of data from a stream.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to read from.
|
||||
* @param {number} size - The number of bytes to read.
|
||||
* @returns {Promise<Buffer|null>} - A Promise that resolves to a Buffer of up to size bytes if available, or null if end of stream is reached. The Promise is rejected if there is an error while reading from the stream.
|
||||
*/
|
||||
const readChunk = (stream, size) =>
|
||||
stream.closed || stream.readableEnded
|
||||
? Promise.resolve(null)
|
||||
@@ -40,13 +33,6 @@ const readChunk = (stream, size) =>
|
||||
})
|
||||
exports.readChunk = readChunk
|
||||
|
||||
/**
|
||||
* Read a chunk of data from a stream.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to read from.
|
||||
* @param {number} size - The number of bytes to read.
|
||||
* @returns {Promise<Buffer>} - A Promise that resolves to a Buffer of size bytes. The Promise is rejected if there is an error while reading from the stream.
|
||||
*/
|
||||
exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
const chunk = await readChunk(stream, size)
|
||||
if (chunk === null) {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/task):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/task
|
||||
```
|
||||
> npm install --save @vates/task
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/toggle-scripts):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/toggle-scripts
|
||||
```
|
||||
> npm install --save @vates/toggle-scripts
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/async-map):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/async-map
|
||||
```
|
||||
> npm install --save @xen-orchestra/async-map
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/audit-core):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/audit-core
|
||||
```
|
||||
> npm install --save @xen-orchestra/audit-core
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups-cli):
|
||||
|
||||
```sh
|
||||
npm install --global @xen-orchestra/backups-cli
|
||||
```
|
||||
> npm install --global @xen-orchestra/backups-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.6",
|
||||
"@xen-orchestra/backups": "^0.29.5",
|
||||
"@xen-orchestra/fs": "^3.3.1",
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/backups
|
||||
```
|
||||
> npm install --save @xen-orchestra/backups
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -258,9 +258,6 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
||||
$defer.onFailure(() => newVdi.$destroy())
|
||||
|
||||
await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
|
||||
if (vdi.virtual_size > newVdi.virtual_size) {
|
||||
await newVdi.$callAsync('resize', vdi.virtual_size)
|
||||
}
|
||||
} else if (vdiRef === vmRecord.suspend_VDI) {
|
||||
// suspendVDI has already created
|
||||
newVdi = suspendVdi
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { finished, PassThrough } = require('node:stream')
|
||||
const eos = require('end-of-stream')
|
||||
const { PassThrough } = require('stream')
|
||||
|
||||
const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStreamUnpipe')
|
||||
|
||||
@@ -8,29 +9,29 @@ const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStr
|
||||
//
|
||||
// in case of error in the new readable stream, it will simply be unpiped
|
||||
// from the original one
|
||||
exports.forkStreamUnpipe = function forkStreamUnpipe(source) {
|
||||
const { forks = 0 } = source
|
||||
source.forks = forks + 1
|
||||
exports.forkStreamUnpipe = function forkStreamUnpipe(stream) {
|
||||
const { forks = 0 } = stream
|
||||
stream.forks = forks + 1
|
||||
|
||||
debug('forking', { forks: source.forks })
|
||||
debug('forking', { forks: stream.forks })
|
||||
|
||||
const fork = new PassThrough()
|
||||
source.pipe(fork)
|
||||
finished(source, { writable: false }, error => {
|
||||
const proxy = new PassThrough()
|
||||
stream.pipe(proxy)
|
||||
eos(stream, error => {
|
||||
if (error !== undefined) {
|
||||
debug('error on original stream, destroying fork', { error })
|
||||
fork.destroy(error)
|
||||
proxy.destroy(error)
|
||||
}
|
||||
})
|
||||
finished(fork, { readable: false }, error => {
|
||||
debug('end of stream, unpiping', { error, forks: --source.forks })
|
||||
eos(proxy, error => {
|
||||
debug('end of stream, unpiping', { error, forks: --stream.forks })
|
||||
|
||||
source.unpipe(fork)
|
||||
stream.unpipe(proxy)
|
||||
|
||||
if (source.forks === 0) {
|
||||
if (stream.forks === 0) {
|
||||
debug('no more forks, destroying original stream')
|
||||
source.destroy(new Error('no more consumers for this stream'))
|
||||
stream.destroy(new Error('no more consumers for this stream'))
|
||||
}
|
||||
})
|
||||
return fork
|
||||
return proxy
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.29.6",
|
||||
"version": "0.29.5",
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.4",
|
||||
"@vates/fuse-vhd": "^1.0.0",
|
||||
"@vates/nbd-client": "^1.0.1",
|
||||
"@vates/nbd-client": "*",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^3.3.1",
|
||||
@@ -32,6 +32,7 @@
|
||||
"compare-versions": "^5.0.1",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"end-of-stream": "^1.4.4",
|
||||
"fs-extra": "^11.1.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
|
||||
@@ -7,8 +7,6 @@ const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
const { decorateClass } = require('@vates/decorate-with')
|
||||
const { defer } = require('golike-defer')
|
||||
const { dirname } = require('path')
|
||||
|
||||
const { formatFilenameDate } = require('../_filenameDate.js')
|
||||
@@ -24,7 +22,7 @@ const NbdClient = require('@vates/nbd-client')
|
||||
|
||||
const { debug, warn, info } = createLogger('xo:backups:DeltaBackupWriter')
|
||||
|
||||
class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
||||
exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
||||
async checkBaseVdis(baseUuidToSrcVdi) {
|
||||
const { handler } = this._adapter
|
||||
const backup = this._backup
|
||||
@@ -135,7 +133,7 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
||||
}
|
||||
}
|
||||
|
||||
async _transfer($defer, { timestamp, deltaExport }) {
|
||||
async _transfer({ timestamp, deltaExport }) {
|
||||
const adapter = this._adapter
|
||||
const backup = this._backup
|
||||
|
||||
@@ -212,11 +210,6 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
||||
debug('got NBD info', { nbdInfo, vdi: id, path })
|
||||
nbdClient = new NbdClient(nbdInfo)
|
||||
await nbdClient.connect()
|
||||
|
||||
// this will inform the xapi that we don't need this anymore
|
||||
// and will detach the vdi from dom0
|
||||
$defer(() => nbdClient.disconnect())
|
||||
|
||||
info('NBD client ready', { vdi: id, path })
|
||||
} catch (error) {
|
||||
nbdClient = undefined
|
||||
@@ -255,6 +248,3 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
|
||||
// TODO: run cleanup?
|
||||
}
|
||||
}
|
||||
exports.DeltaBackupWriter = decorateClass(DeltaBackupWriter, {
|
||||
_transfer: defer,
|
||||
})
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cr-seed-cli):
|
||||
|
||||
```sh
|
||||
npm install --global @xen-orchestra/cr-seed-cli
|
||||
```
|
||||
> npm install --global @xen-orchestra/cr-seed-cli
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.5.1",
|
||||
"xen-api": "^1.2.5"
|
||||
"xen-api": "^1.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cron):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/cron
|
||||
```
|
||||
> npm install --save @xen-orchestra/cron
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/defined):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/defined
|
||||
```
|
||||
> npm install --save @xen-orchestra/defined
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/emit-async):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/emit-async
|
||||
```
|
||||
> npm install --save @xen-orchestra/emit-async
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/fs):
|
||||
|
||||
```sh
|
||||
npm install --global @xen-orchestra/fs
|
||||
```
|
||||
> npm install --global @xen-orchestra/fs
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -24,14 +24,12 @@
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"echarts": "^5.3.3",
|
||||
"highlight.js": "^11.6.0",
|
||||
"human-format": "^1.0.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json5": "^2.2.1",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"make-error": "^1.3.6",
|
||||
"markdown-it": "^13.0.1",
|
||||
"pinia": "^2.0.14",
|
||||
"placement.js": "^1.0.0-beta.5",
|
||||
"vue": "^3.2.37",
|
||||
|
||||
@@ -16,8 +16,8 @@ a {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
code, code * {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.card-view {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div ref="rootElement" class="app-markdown" v-html="html" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Ref, computed, ref } from "vue";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import { markdown } from "@/libs/markdown";
|
||||
|
||||
const rootElement = ref() as Ref<HTMLElement>;
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
}>();
|
||||
|
||||
const html = computed(() => markdown.render(props.content ?? ""));
|
||||
|
||||
useEventListener(
|
||||
rootElement,
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (!target.classList.contains("copy-button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const copyable =
|
||||
target.parentElement!.querySelector<HTMLElement>(".copyable");
|
||||
|
||||
if (copyable !== null) {
|
||||
navigator.clipboard.writeText(copyable.innerText);
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.app-markdown {
|
||||
font-size: 1.6rem;
|
||||
|
||||
:deep() {
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1.4rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
code:not(.hljs-code) {
|
||||
background-color: var(--background-color-extra-blue);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: revert;
|
||||
padding-left: 2rem;
|
||||
list-style-type: revert;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
thead th {
|
||||
border-bottom: 2px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 1rem;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-d20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<pre class="code-highlight hljs"><code v-html="codeAsHtml"></code></pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HLJS from "highlight.js";
|
||||
import { computed } from "vue";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
code?: any;
|
||||
lang?: string;
|
||||
}>(),
|
||||
{ lang: "typescript" }
|
||||
);
|
||||
|
||||
const codeAsText = computed(() => {
|
||||
switch (typeof props.code) {
|
||||
case "string":
|
||||
return props.code;
|
||||
case "function":
|
||||
return String(props.code);
|
||||
default:
|
||||
return JSON.stringify(props.code, undefined, 2);
|
||||
}
|
||||
});
|
||||
|
||||
const codeAsHtml = computed(
|
||||
() => HLJS.highlight(codeAsText.value, { language: props.lang }).value
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.code-highlight {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.6rem;
|
||||
text-align: left;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
@remove="emit('removeSort', property)"
|
||||
>
|
||||
<span class="property">
|
||||
<UiIcon :icon="isAscending ? faCaretUp : faCaretDown" />
|
||||
<FontAwesomeIcon :icon="isAscending ? faCaretUp : faCaretDown" />
|
||||
{{ property }}
|
||||
</span>
|
||||
</UiFilter>
|
||||
@@ -17,7 +17,7 @@
|
||||
</UiActionButton>
|
||||
</UiFilterGroup>
|
||||
|
||||
<UiModal v-if="isOpen" :icon="faSort" @submit.prevent="handleSubmit">
|
||||
<UiModal v-if="isOpen" @submit.prevent="handleSubmit" :icon="faSort">
|
||||
<div class="form-widgets">
|
||||
<FormWidget :label="$t('sort-by')">
|
||||
<select v-model="newSortProperty">
|
||||
@@ -48,14 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import UiActionButton from "@/components/ui/UiActionButton.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiFilter from "@/components/ui/UiFilter.vue";
|
||||
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { ref } from "vue";
|
||||
import type { ActiveSorts, Sorts } from "@/types/sort";
|
||||
import {
|
||||
faCaretDown,
|
||||
@@ -63,7 +56,13 @@ import {
|
||||
faPlus,
|
||||
faSort,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { ref } from "vue";
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiActionButton from "@/components/ui/UiActionButton.vue";
|
||||
import UiFilter from "@/components/ui/UiFilter.vue";
|
||||
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
|
||||
defineProps<{
|
||||
availableSorts: Sorts;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<th>
|
||||
<div class="content">
|
||||
<span class="label">
|
||||
<UiIcon :icon="icon" />
|
||||
<FontAwesomeIcon v-if="icon" :icon="icon" />
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
@@ -10,7 +10,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<span class="widget">
|
||||
<span v-if="before || $slots.before" class="before">
|
||||
<slot name="before">
|
||||
<UiIcon v-if="isIcon(before)" :icon="before" fixed-width />
|
||||
<FontAwesomeIcon v-if="isIcon(before)" :icon="before" fixed-width />
|
||||
<template v-else>{{ before }}</template>
|
||||
</slot>
|
||||
</span>
|
||||
@@ -17,7 +17,7 @@
|
||||
</span>
|
||||
<span v-if="after || $slots.after" class="after">
|
||||
<slot name="after">
|
||||
<UiIcon v-if="isIcon(after)" :icon="after" fixed-width />
|
||||
<FontAwesomeIcon v-if="isIcon(after)" :icon="after" fixed-width />
|
||||
<template v-else>{{ after }}</template>
|
||||
</slot>
|
||||
</span>
|
||||
@@ -26,7 +26,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<UiIcon :class="className" :icon="icon" class="power-state-icon" />
|
||||
<FontAwesomeIcon class="power-state-icon" :class="className" :icon="icon" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { PowerState } from "@/libs/xen-api";
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
faMoon,
|
||||
faPause,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
faQuestion,
|
||||
faStop,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { computed } from "vue";
|
||||
import type { PowerState } from "@/libs/xen-api";
|
||||
|
||||
const props = defineProps<{
|
||||
state: PowerState;
|
||||
@@ -30,7 +29,7 @@ const icon = computed(() => icons[props.state] ?? faQuestion);
|
||||
const className = computed(() => `state-${props.state.toLocaleLowerCase()}`);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped lang="postcss">
|
||||
.power-state-icon {
|
||||
color: var(--color-extra-blue-d60);
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
v-slot="{ isActive, href }"
|
||||
:to="disabled || isTabBarDisabled ? '' : to"
|
||||
custom
|
||||
>
|
||||
<UiTab :active="isActive" :disabled="disabled" :href="href" tag="a">
|
||||
<slot />
|
||||
</UiTab>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import UiTab from "@/components/ui/UiTab.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
to: RouteLocationRaw;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const isTabBarDisabled = inject<ComputedRef<boolean>>(
|
||||
"isTabBarDisabled",
|
||||
computed(() => false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,29 +1,16 @@
|
||||
<template>
|
||||
<div class="ui-tab-bar">
|
||||
<div class="tab-bar">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
provide(
|
||||
"isTabBarDisabled",
|
||||
computed(() => props.disabled)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-tab-bar {
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 6.5rem;
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-primary);
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
54
@xen-orchestra/lite/src/components/TabBarItem.vue
Normal file
54
@xen-orchestra/lite/src/components/TabBarItem.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<span v-if="disabled" class="tab-bar-item disabled">
|
||||
<slot />
|
||||
</span>
|
||||
<RouterLink v-else class="tab-bar-item" v-bind="$props">
|
||||
<slot />
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouterLinkProps } from "vue-router";
|
||||
|
||||
// https://vuejs.org/api/sfc-script-setup.html#type-only-props-emit-declarations
|
||||
interface Props extends RouterLinkProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.tab-bar-item {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.2em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-blue-scale-100);
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--color-blue-scale-400);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="title-bar">
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<FontAwesomeIcon :icon="icon" class="icon" />
|
||||
<div class="title">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -11,7 +11,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from "vue";
|
||||
import { percent } from "@/libs/utils";
|
||||
import { computed, type ComputedRef, inject } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
total: number;
|
||||
@@ -34,9 +34,7 @@ const freePercent = computed(() =>
|
||||
percent(props.total - props.used, props.total)
|
||||
);
|
||||
|
||||
const valueFormatter = inject("valueFormatter") as ComputedRef<
|
||||
(value: number) => string
|
||||
>;
|
||||
const valueFormatter = inject("valueFormatter") as (value: number) => string;
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -33,17 +33,8 @@ const props = defineProps<{
|
||||
maxValue?: number;
|
||||
}>();
|
||||
|
||||
const valueFormatter = computed(() => {
|
||||
const formatter = props.valueFormatter;
|
||||
|
||||
return (value: OptionDataValue | OptionDataValue[]) => {
|
||||
if (formatter) {
|
||||
return formatter(value as number);
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
};
|
||||
});
|
||||
const valueFormatter = (value: OptionDataValue | OptionDataValue[]) =>
|
||||
props.valueFormatter?.(value as number) ?? `${value}`;
|
||||
|
||||
provide("valueFormatter", valueFormatter);
|
||||
|
||||
@@ -65,7 +56,7 @@ const option = computed<EChartsOption>(() => ({
|
||||
data: props.data.map((series) => series.label),
|
||||
},
|
||||
tooltip: {
|
||||
valueFormatter: valueFormatter.value,
|
||||
valueFormatter,
|
||||
},
|
||||
xAxis: {
|
||||
type: "time",
|
||||
@@ -79,9 +70,9 @@ const option = computed<EChartsOption>(() => ({
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLabel: {
|
||||
formatter: valueFormatter.value,
|
||||
formatter: valueFormatter,
|
||||
},
|
||||
max: props.maxValue ?? Y_AXIS_MAX_VALUE,
|
||||
max: () => props.maxValue ?? Y_AXIS_MAX_VALUE,
|
||||
},
|
||||
series: props.data.map((series, index) => ({
|
||||
type: "line",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
>
|
||||
<input
|
||||
v-model="value"
|
||||
:class="{ indeterminate: type === 'checkbox' && value === undefined }"
|
||||
:disabled="isLabelDisabled || disabled"
|
||||
:type="type === 'radio' ? 'radio' : 'checkbox'"
|
||||
class="input"
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
inject,
|
||||
ref,
|
||||
} from "vue";
|
||||
import { faCheck, faCircle, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
@@ -54,17 +53,7 @@ const value = useVModel(props, "modelValue", emit);
|
||||
const type = inject<"checkbox" | "radio" | "toggle">("inputType", "checkbox");
|
||||
const hasLabel = inject("hasLabel", false);
|
||||
const isLabelDisabled = inject("isLabelDisabled", ref(false));
|
||||
const icon = computed(() => {
|
||||
if (type !== "checkbox") {
|
||||
return faCircle;
|
||||
}
|
||||
|
||||
if (value.value === undefined) {
|
||||
return faMinus;
|
||||
}
|
||||
|
||||
return faCheck;
|
||||
});
|
||||
const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -85,11 +74,6 @@ const icon = computed(() => {
|
||||
.form-checkbox {
|
||||
--checkbox-border-radius: 0.25em;
|
||||
--checkbox-icon-size: 1em;
|
||||
|
||||
.input.indeterminate + .fake-checkbox > .icon {
|
||||
opacity: 1;
|
||||
color: var(--color-blue-scale-300);
|
||||
}
|
||||
}
|
||||
|
||||
.form-checkbox,
|
||||
@@ -125,8 +109,8 @@ const icon = computed(() => {
|
||||
}
|
||||
|
||||
.icon {
|
||||
transition: transform 0.125s ease-in-out;
|
||||
transform: translateX(-0.7em);
|
||||
transition: transform 0.125s ease-in-out;
|
||||
}
|
||||
|
||||
.input:checked + .fake-checkbox > .icon {
|
||||
@@ -156,12 +140,12 @@ const icon = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.25em;
|
||||
transition: background-color 0.125s ease-in-out,
|
||||
border-color 0.125s ease-in-out;
|
||||
border: var(--checkbox-border-width) solid var(--border-color);
|
||||
border-radius: var(--checkbox-border-radius);
|
||||
background-color: var(--background-color);
|
||||
box-shadow: var(--shadow-100);
|
||||
transition: background-color 0.125s ease-in-out,
|
||||
border-color 0.125s ease-in-out;
|
||||
|
||||
--border-color: var(--color-blue-scale-400);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<span :class="wrapperClass" v-bind="wrapperAttrs">
|
||||
<template v-if="inputType === 'select'">
|
||||
<input
|
||||
v-if="!isSelect"
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="input"
|
||||
ref="inputElement"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<template v-else>
|
||||
<select
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
@@ -15,24 +24,6 @@
|
||||
<UiIcon :fixed-width="false" :icon="faAngleDown" />
|
||||
</span>
|
||||
</template>
|
||||
<textarea
|
||||
v-else-if="inputType === 'textarea'"
|
||||
ref="textarea"
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="textarea"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="input"
|
||||
ref="inputElement"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<span v-if="before !== undefined" class="before">
|
||||
<template v-if="typeof before === 'string'">{{ before }}</template>
|
||||
<UiIcon v-else :icon="before" class="before" />
|
||||
@@ -52,19 +43,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isEmpty } from "lodash-es";
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type InputHTMLAttributes,
|
||||
computed,
|
||||
inject,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import type { Color } from "@/types";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useTextareaAutosize, useVModel } from "@vueuse/core";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
// Temporary workaround for https://github.com/vuejs/core/issues/4294
|
||||
@@ -89,10 +79,8 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
const isEmpty = computed(
|
||||
() => props.modelValue == null || String(props.modelValue).trim() === ""
|
||||
);
|
||||
const inputType = inject("inputType", "input");
|
||||
const empty = computed(() => isEmpty(props.modelValue));
|
||||
const isSelect = inject("isSelect", false);
|
||||
const isLabelDisabled = inject("isLabelDisabled", ref(false));
|
||||
const color = inject(
|
||||
"color",
|
||||
@@ -100,10 +88,10 @@ const color = inject(
|
||||
);
|
||||
|
||||
const wrapperClass = computed(() => [
|
||||
`form-${inputType}`,
|
||||
isSelect ? "form-select" : "form-input",
|
||||
{
|
||||
disabled: props.disabled || isLabelDisabled.value,
|
||||
empty: isEmpty.value,
|
||||
empty: empty.value,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -116,12 +104,6 @@ const inputClass = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const { textarea, triggerResize } = useTextareaAutosize();
|
||||
|
||||
watch(value, () => nextTick(() => triggerResize()), {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const focus = () => inputElement.value.focus();
|
||||
|
||||
defineExpose({
|
||||
@@ -131,13 +113,12 @@ defineExpose({
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
.form-select {
|
||||
display: inline-grid;
|
||||
align-items: stretch;
|
||||
|
||||
--before-width: v-bind('beforeWidth || "1.75em"');
|
||||
--after-width: v-bind('afterWidth || "1.625em"');
|
||||
--before-width: v-bind('beforeWidth ?? "1.75em"');
|
||||
--after-width: v-bind('afterWidth ?? "1.625em"');
|
||||
--caret-width: 1.5em;
|
||||
|
||||
--text-color: var(--color-blue-scale-100);
|
||||
@@ -151,8 +132,7 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
.form-input {
|
||||
grid-template-columns: var(--before-width) auto var(--after-width);
|
||||
}
|
||||
|
||||
@@ -165,10 +145,8 @@ defineExpose({
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea,
|
||||
.select {
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
@@ -257,19 +235,8 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
height: auto;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea {
|
||||
padding-right: 0.625em;
|
||||
padding-left: 0.625em;
|
||||
padding: 0 0.625em 0 0.625em;
|
||||
|
||||
&.has-before {
|
||||
padding-left: calc(var(--before-width) + 0.25em);
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<UiModal
|
||||
@submit.prevent="saveJson"
|
||||
:color="isJsonValid ? 'success' : 'error'"
|
||||
v-if="isCodeModalOpen"
|
||||
:icon="faCode"
|
||||
@close="closeCodeModal"
|
||||
>
|
||||
<FormTextarea class="modal-textarea" v-model="editedJson" />
|
||||
<template #buttons>
|
||||
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
|
||||
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
|
||||
<UiButton :disabled="!isJsonValid" type="submit"
|
||||
>{{ $t("save") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
<FormInput
|
||||
@click="openCodeModal"
|
||||
:model-value="jsonValue"
|
||||
:before="faCode"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormTextarea from "@/components/form/FormTextarea.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { faCode } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel, whenever } from "@vueuse/core";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: any): void;
|
||||
}>();
|
||||
|
||||
const model = useVModel(props, "modelValue", emit);
|
||||
|
||||
const {
|
||||
open: openCodeModal,
|
||||
close: closeCodeModal,
|
||||
isOpen: isCodeModalOpen,
|
||||
} = useModal();
|
||||
|
||||
const jsonValue = computed(() => JSON.stringify(model.value, undefined, 2));
|
||||
|
||||
const isJsonValid = computed(() => {
|
||||
try {
|
||||
JSON.parse(editedJson.value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const formatJson = () => {
|
||||
if (!isJsonValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
editedJson.value = JSON.stringify(JSON.parse(editedJson.value), undefined, 2);
|
||||
};
|
||||
|
||||
const saveJson = () => {
|
||||
if (!isJsonValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formatJson();
|
||||
|
||||
model.value = JSON.parse(editedJson.value);
|
||||
|
||||
closeCodeModal();
|
||||
};
|
||||
|
||||
whenever(isCodeModalOpen, () => (editedJson.value = jsonValue.value));
|
||||
|
||||
const editedJson = ref();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
:deep(.modal-textarea) {
|
||||
min-width: 50rem;
|
||||
min-height: 20rem;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
import { provide } from "vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
|
||||
provide("inputType", "select");
|
||||
provide("isSelect", true);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<FormInput />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { provide } from "vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
|
||||
provide("inputType", "textarea");
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<div class="infra-action">
|
||||
<slot>
|
||||
<UiIcon :icon="icon" fixed-width />
|
||||
<FontAwesomeIcon :icon="icon" fixed-width />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<a :href="href" class="link" @click="navigate">
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<FontAwesomeIcon :icon="icon" class="icon" />
|
||||
<div class="text">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -21,9 +21,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
icon: IconDefinition;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<li class="infra-loading-item">
|
||||
<div class="infra-item-label-placeholder">
|
||||
<div class="link-placeholder">
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<FontAwesomeIcon :icon="icon" class="icon" />
|
||||
<div class="loader"> </div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,7 +10,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -1,40 +1,67 @@
|
||||
<template>
|
||||
<UiTabBar :disabled="!isReady">
|
||||
<RouterTab :to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }">
|
||||
<TabBar>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("dashboard") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.alarms', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.alarms', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("alarms") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.stats', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.stats', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("stats") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.system', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.system', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("system") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.network', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.network', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("network") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.storage', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.storage', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("storage") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.tasks', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.tasks', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("tasks") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.hosts', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.hosts', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("hosts") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.vms', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.vms', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("vms") }}
|
||||
</RouterTab>
|
||||
</UiTabBar>
|
||||
</TabBarItem>
|
||||
</TabBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import RouterTab from "@/components/RouterTab.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
import TabBar from "@/components/TabBar.vue";
|
||||
import TabBarItem from "@/components/TabBarItem.vue";
|
||||
import { usePoolStore } from "@/stores/pool.store";
|
||||
|
||||
const poolStore = usePoolStore();
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<tr class="finished-task-row" :class="{ finished: !isPending }">
|
||||
<td>{{ task.name_label }}</td>
|
||||
<td>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'host.dashboard',
|
||||
params: { uuid: host.uuid },
|
||||
}"
|
||||
>
|
||||
{{ host.name_label }}
|
||||
</RouterLink>
|
||||
</td>
|
||||
<td>
|
||||
<UiProgressBar v-if="isPending" :max-value="1" :value="task.progress" />
|
||||
</td>
|
||||
<td>
|
||||
<RelativeTime v-if="isPending" :date="createdAt" />
|
||||
<template v-else>{{ $d(createdAt, "datetime_short") }}</template>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="finishedAt !== undefined">
|
||||
{{ $d(finishedAt, "datetime_short") }}
|
||||
</template>
|
||||
<RelativeTime
|
||||
v-else-if="isPending && estimatedEndAt !== Infinity"
|
||||
:date="estimatedEndAt"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RelativeTime from "@/components/RelativeTime.vue";
|
||||
import UiProgressBar from "@/components/ui/UiProgressBar.vue";
|
||||
import { parseDateTime } from "@/libs/utils";
|
||||
import type { XenApiTask } from "@/libs/xen-api";
|
||||
import { useHostStore } from "@/stores/host.store";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
isPending?: boolean;
|
||||
task: XenApiTask;
|
||||
}>();
|
||||
|
||||
const { getRecord: getHost } = useHostStore();
|
||||
|
||||
const createdAt = computed(() => parseDateTime(props.task.created));
|
||||
|
||||
const host = computed(() => getHost(props.task.resident_on));
|
||||
|
||||
const estimatedEndAt = computed(
|
||||
() =>
|
||||
createdAt.value +
|
||||
(new Date().getTime() - createdAt.value) / props.task.progress
|
||||
);
|
||||
|
||||
const finishedAt = computed(() =>
|
||||
props.isPending ? undefined : parseDateTime(props.task.finished)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.finished {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<UiTable class="tasks-table">
|
||||
<template #header>
|
||||
<th>{{ $t("name") }}</th>
|
||||
<th>{{ $t("object") }}</th>
|
||||
<th>{{ $t("task.progress") }}</th>
|
||||
<th>{{ $t("task.started") }}</th>
|
||||
<th>{{ $t("task.estimated-end") }}</th>
|
||||
</template>
|
||||
|
||||
<TaskRow
|
||||
v-for="task in pendingTasks"
|
||||
:key="task.uuid"
|
||||
:task="task"
|
||||
is-pending
|
||||
/>
|
||||
<TaskRow v-for="task in finishedTasks" :key="task.uuid" :task="task" />
|
||||
</UiTable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TaskRow from "@/components/tasks/TaskRow.vue";
|
||||
import UiTable from "@/components/ui/UiTable.vue";
|
||||
import type { XenApiTask } from "@/libs/xen-api";
|
||||
|
||||
defineProps<{
|
||||
pendingTasks: XenApiTask[];
|
||||
finishedTasks: XenApiTask[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -5,7 +5,7 @@
|
||||
:type="type || 'button'"
|
||||
class="ui-button"
|
||||
>
|
||||
<UiSpinner v-if="isBusy" />
|
||||
<span v-if="isBusy" class="loader" />
|
||||
<template v-else>
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<slot />
|
||||
@@ -14,7 +14,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
import { computed, inject, unref } from "vue";
|
||||
import type { Color } from "@/types";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<span :class="color" class="ui-counter">
|
||||
<span :class="{ overflow: value > 99 }">
|
||||
{{ value }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Color } from "@/types";
|
||||
|
||||
defineProps<{
|
||||
value: number;
|
||||
color?: Color;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-counter {
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
color: var(--color-blue-scale-500);
|
||||
border-radius: calc(var(--size) / 2);
|
||||
background-color: var(--color-blue-scale-300);
|
||||
--size: 1.75em;
|
||||
|
||||
.overflow {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: var(--color-green-infra-base);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: var(--color-orange-world-base);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: var(--color-red-vates-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,13 +4,12 @@
|
||||
<slot />
|
||||
</span>
|
||||
<span class="remove" @click.stop="emit('remove')">
|
||||
<UiIcon :icon="faRemove" />
|
||||
<FontAwesomeIcon :icon="faRemove" class="icon" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import { faRemove } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -40,7 +39,6 @@ const emit = defineEmits<{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -56,7 +54,6 @@ const emit = defineEmits<{
|
||||
width: 2.8rem;
|
||||
margin: 0.2rem;
|
||||
background-color: var(--color-extra-blue-l40);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-red-vates-l20);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<UiSpinner v-if="busy" class="ui-icon" />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="icon !== undefined"
|
||||
v-if="icon !== undefined"
|
||||
:icon="icon"
|
||||
:spin="busy"
|
||||
class="ui-icon"
|
||||
:fixed-width="fixedWidth"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
import { computed } from "vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
busy?: boolean;
|
||||
icon?: IconDefinition;
|
||||
@@ -21,6 +21,8 @@ withDefaults(
|
||||
}>(),
|
||||
{ fixedWidth: true }
|
||||
);
|
||||
|
||||
const icon = computed(() => (props.busy ? faSpinner : props.icon));
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
>
|
||||
<div class="container">
|
||||
<span v-if="onClose" class="close-icon" @click="emit('close')">
|
||||
<UiIcon :icon="faXmark" />
|
||||
<FontAwesomeIcon :icon="faXmark" />
|
||||
</span>
|
||||
<div v-if="icon || $slots.icon" class="modal-icon">
|
||||
<slot name="icon">
|
||||
<UiIcon :icon="icon" />
|
||||
<FontAwesomeIcon :icon="icon" />
|
||||
</slot>
|
||||
</div>
|
||||
<UiTitle v-if="$slots.title" type="h4">
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
:class="{ active, disabled: disabled || isTabBarDisabled }"
|
||||
class="ui-tab"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
tag?: string;
|
||||
}>(),
|
||||
{ tag: "span" }
|
||||
);
|
||||
|
||||
const isTabBarDisabled = inject<ComputedRef<boolean>>(
|
||||
"isTabBarDisabled",
|
||||
computed(() => false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-tab {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.2em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-blue-scale-100);
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--color-blue-scale-400);
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,8 +18,8 @@
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
:deep(th),
|
||||
:deep(td) {
|
||||
:slotted(th),
|
||||
:slotted(td) {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid lightgrey;
|
||||
border-right: 1px solid lightgrey;
|
||||
@@ -30,14 +30,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.header-row th) {
|
||||
:slotted(.header-row th) {
|
||||
color: var(--color-extra-blue-base);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
:deep(.body td) {
|
||||
:slotted(.body td) {
|
||||
font-weight: 400;
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import type { MaybeRef } from "@vueuse/core";
|
||||
import { differenceBy } from "lodash-es";
|
||||
import { type Ref, ref, unref, watch } from "vue";
|
||||
|
||||
export default function useArrayRemovedItemsHistory<T>(
|
||||
list: Ref<T[]>,
|
||||
iteratee: (item: T) => unknown = (item) => item,
|
||||
options: {
|
||||
limit?: MaybeRef<number>;
|
||||
onRemove?: (items: T[]) => any[];
|
||||
} = {}
|
||||
limit = Infinity,
|
||||
iteratee: (item: T) => unknown = (item) => item
|
||||
) {
|
||||
const currentList: Ref<T[]> = ref([]);
|
||||
const history: Ref<T[]> = ref([]);
|
||||
const { limit = Infinity, onRemove = (items) => items } = options;
|
||||
|
||||
watch(
|
||||
list,
|
||||
@@ -24,10 +19,10 @@ export default function useArrayRemovedItemsHistory<T>(
|
||||
|
||||
watch(currentList, (nextList, previousList) => {
|
||||
const removedItems = differenceBy(previousList, nextList, iteratee);
|
||||
history.value.push(...onRemove(removedItems));
|
||||
history.value.push(...removedItems);
|
||||
const currentLimit = unref(limit);
|
||||
if (history.value.length > currentLimit) {
|
||||
history.value = history.value.slice(-currentLimit);
|
||||
history.value.slice(-currentLimit);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import HLJS from "highlight.js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
export const markdown = new MarkdownIt();
|
||||
|
||||
markdown.set({
|
||||
highlight: (str: string, lang: string) => {
|
||||
const code = highlight(str, lang);
|
||||
return `<pre class="hljs"><button class="copy-button" type="button">Copy</button><code class="hljs-code">${code}</code></pre>`;
|
||||
},
|
||||
});
|
||||
|
||||
function highlight(str: string, lang: string) {
|
||||
switch (lang) {
|
||||
case "vue-template": {
|
||||
const indented = str
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((s) => ` ${s}`)
|
||||
.join("\n");
|
||||
return wrap(indented, "template");
|
||||
}
|
||||
case "vue-script":
|
||||
return wrap(str.trim(), "script");
|
||||
case "vue-style":
|
||||
return wrap(str.trim(), "style");
|
||||
default: {
|
||||
if (HLJS.getLanguage(lang) !== undefined) {
|
||||
return copyable(HLJS.highlight(str, { language: lang }).value);
|
||||
}
|
||||
|
||||
return copyable(markdown.utils.escapeHtml(str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wrap(str: string, tag: "template" | "script" | "style") {
|
||||
let openTag;
|
||||
let code;
|
||||
|
||||
switch (tag) {
|
||||
case "template":
|
||||
openTag = "<template>";
|
||||
code = HLJS.highlight(str, { language: "xml" }).value;
|
||||
break;
|
||||
case "script":
|
||||
openTag = '<script lang="ts" setup>';
|
||||
code = HLJS.highlight(str, { language: "typescript" }).value;
|
||||
break;
|
||||
case "style":
|
||||
openTag = '<style lang="postcss" scoped>';
|
||||
code = HLJS.highlight(str, { language: "scss" }).value;
|
||||
break;
|
||||
}
|
||||
|
||||
const openTagHtml = HLJS.highlight(openTag, { language: "xml" }).value;
|
||||
const closeTagHtml = HLJS.highlight(`</${tag}>`, { language: "xml" }).value;
|
||||
|
||||
return `${openTagHtml}${copyable(code)}${closeTagHtml}`;
|
||||
}
|
||||
|
||||
function copyable(code: string) {
|
||||
return `<div class="copyable">${code}</div>`;
|
||||
}
|
||||
@@ -56,7 +56,6 @@ export function getFilterIcon(filter: Filter | undefined) {
|
||||
}
|
||||
|
||||
export function parseDateTime(dateTime: string) {
|
||||
dateTime = dateTime.replace(/(-|\.\d{3})/g, ""); // Allow toISOString() date-time format
|
||||
const date = utcParse("%Y%m%dT%H:%M:%SZ")(dateTime);
|
||||
if (date === null) {
|
||||
throw new RangeError(
|
||||
|
||||
@@ -115,15 +115,6 @@ export type XenApiVmMetrics = XenApiRecord;
|
||||
|
||||
export type XenApiVmGuestMetrics = XenApiRecord;
|
||||
|
||||
export interface XenApiTask extends XenApiRecord {
|
||||
name_label: string;
|
||||
resident_on: string;
|
||||
created: string;
|
||||
finished: string;
|
||||
status: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
type WatchCallbackResult = {
|
||||
id: string;
|
||||
class: ObjectType;
|
||||
|
||||
@@ -36,14 +36,12 @@
|
||||
"log-out": "Log out",
|
||||
"login": "Login",
|
||||
"migrate": "Migrate",
|
||||
"name": "Name",
|
||||
"network": "Network",
|
||||
"network-download": "Download",
|
||||
"network-throughput": "Network throughput",
|
||||
"network-upload": "Upload",
|
||||
"news": "News",
|
||||
"news-name": "{name} news",
|
||||
"object": "Object",
|
||||
"object-not-found": "Object {id} can't be found…",
|
||||
"or": "Or",
|
||||
"page-not-found": "This page is not to be found…",
|
||||
@@ -55,7 +53,6 @@
|
||||
"property": "Property",
|
||||
"ram-usage": "RAM usage",
|
||||
"reboot": "Reboot",
|
||||
"reformat": "Reformat",
|
||||
"relative-time": {
|
||||
"day": "1 day | {n} days",
|
||||
"future": "In {str}",
|
||||
@@ -68,14 +65,13 @@
|
||||
"year": "1 year | {n} years"
|
||||
},
|
||||
"resume": "Resume",
|
||||
"save": "Save",
|
||||
"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",
|
||||
"stacked-ram-usage": "Stacked memory usage",
|
||||
"start": "Start",
|
||||
"start-on-host": "Start on specific host",
|
||||
"stats": "Stats",
|
||||
@@ -85,12 +81,6 @@
|
||||
"suspend": "Suspend",
|
||||
"switch-theme": "Switch theme",
|
||||
"system": "System",
|
||||
"task": {
|
||||
"estimated-end": "Estimated end",
|
||||
"page-title": "Tasks | (1) Tasks | ({n}) Tasks",
|
||||
"progress": "Progress",
|
||||
"started": "Started"
|
||||
},
|
||||
"tasks": "Tasks",
|
||||
"theme-auto": "Auto",
|
||||
"theme-dark": "Dark",
|
||||
|
||||
@@ -36,14 +36,12 @@
|
||||
"log-out": "Se déconnecter",
|
||||
"login": "Connexion",
|
||||
"migrate": "Migrer",
|
||||
"name": "Nom",
|
||||
"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": "Objet",
|
||||
"object-not-found": "L'objet {id} est introuvable…",
|
||||
"or": "Ou",
|
||||
"page-not-found": "Cette page est introuvable…",
|
||||
@@ -55,7 +53,6 @@
|
||||
"property": "Propriété",
|
||||
"ram-usage": "Utilisation de la RAM",
|
||||
"reboot": "Redémarrer",
|
||||
"reformat": "Reformater",
|
||||
"relative-time": {
|
||||
"day": "1 jour | {n} jours",
|
||||
"future": "Dans {str}",
|
||||
@@ -68,14 +65,13 @@
|
||||
"year": "1 an | {n} ans"
|
||||
},
|
||||
"resume": "Reprendre",
|
||||
"save": "Enregistrer",
|
||||
"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",
|
||||
"stacked-ram-usage": "Utilisation de la mémoire empilée",
|
||||
"start": "Démarrer",
|
||||
"start-on-host": "Démarrer sur un hôte spécifique",
|
||||
"stats": "Stats",
|
||||
@@ -85,12 +81,6 @@
|
||||
"suspend": "Suspendre",
|
||||
"switch-theme": "Changer de thème",
|
||||
"system": "Système",
|
||||
"task": {
|
||||
"estimated-end": "Fin estimée",
|
||||
"page-title": "Tâches | (1) Tâches | ({n}) Tâches",
|
||||
"progress": "Progression",
|
||||
"started": "Démarré"
|
||||
},
|
||||
"tasks": "Tâches",
|
||||
"theme-auto": "Auto",
|
||||
"theme-dark": "Sombre",
|
||||
|
||||
@@ -3,11 +3,13 @@ import { createApp } from "vue";
|
||||
import App from "@/App.vue";
|
||||
import i18n from "@/i18n";
|
||||
import router from "@/router";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(i18n);
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.component("FontAwesomeIcon", FontAwesomeIcon);
|
||||
|
||||
app.mount("#root");
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { XenApiTask } from "@/libs/xen-api";
|
||||
import { createRecordContext } from "@/stores/index";
|
||||
|
||||
export const useTaskStore = defineStore("task", () =>
|
||||
createRecordContext<XenApiTask>("task")
|
||||
);
|
||||
@@ -10,7 +10,6 @@ import { useHostStore } from "@/stores/host.store";
|
||||
import { usePoolStore } from "@/stores/pool.store";
|
||||
import { useRecordsStore } from "@/stores/records.store";
|
||||
import { useSrStore } from "@/stores/storage.store";
|
||||
import { useTaskStore } from "@/stores/task.store";
|
||||
import { useVmGuestMetricsStore } from "@/stores/vm-guest-metrics.store";
|
||||
import { useVmMetricsStore } from "@/stores/vm-metrics.store";
|
||||
import { useVmStore } from "@/stores/vm.store";
|
||||
@@ -87,9 +86,6 @@ export const useXenApiStore = defineStore("xen-api", () => {
|
||||
srStore.init(),
|
||||
]);
|
||||
|
||||
const taskStore = useTaskStore();
|
||||
taskStore.init();
|
||||
|
||||
const consoleStore = useConsoleStore();
|
||||
consoleStore.init();
|
||||
}
|
||||
|
||||
@@ -126,7 +126,6 @@ onMounted(() => {
|
||||
.item {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -1,82 +1 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiTitle class="title-with-counter" type="h4">
|
||||
{{ $t("tasks") }}
|
||||
<UiCounter :value="pendingTasks.length" color="info" />
|
||||
</UiTitle>
|
||||
|
||||
<TasksTable :pending-tasks="pendingTasks" :finished-tasks="finishedTasks" />
|
||||
<UiSpinner class="loader" v-if="!isReady" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TasksTable from "@/components/tasks/TasksTable.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiCounter from "@/components/ui/UiCounter.vue";
|
||||
import UiSpinner from "@/components/ui/UiSpinner.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
|
||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||
import useCollectionSorter from "@/composables/collection-sorter.composable";
|
||||
import useFilteredCollection from "@/composables/filtered-collection.composable";
|
||||
import useSortedCollection from "@/composables/sorted-collection.composable";
|
||||
import type { XenApiTask } from "@/libs/xen-api";
|
||||
import { useTaskStore } from "@/stores/task.store";
|
||||
import { useTitle } from "@vueuse/core";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { allRecords, isReady } = storeToRefs(useTaskStore());
|
||||
const { t } = useI18n();
|
||||
|
||||
const { compareFn } = useCollectionSorter<XenApiTask>({
|
||||
initialSorts: ["-created"],
|
||||
});
|
||||
|
||||
const allTasks = useSortedCollection(allRecords, compareFn);
|
||||
|
||||
const { predicate } = useCollectionFilter({
|
||||
initialFilters: ["!name_label:|(SR.scan host.call_plugin)", "status:pending"],
|
||||
});
|
||||
|
||||
const pendingTasks = useFilteredCollection<XenApiTask>(allTasks, predicate);
|
||||
|
||||
const finishedTasks = useArrayRemovedItemsHistory(
|
||||
allTasks,
|
||||
(task) => task.uuid,
|
||||
{
|
||||
limit: 50,
|
||||
onRemove: (tasks) =>
|
||||
tasks.map((task) => ({
|
||||
...task,
|
||||
finished: new Date().toISOString(),
|
||||
})),
|
||||
}
|
||||
);
|
||||
|
||||
useTitle(
|
||||
computed(() => t("task.page-title", { n: pendingTasks.value.length }))
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.title-with-counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.ui-counter {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
color: var(--color-extra-blue-base);
|
||||
display: block;
|
||||
font-size: 4rem;
|
||||
margin: 2rem auto 0;
|
||||
}
|
||||
</style>
|
||||
<template>Tasks (coming soon)</template>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/log):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/log
|
||||
```
|
||||
> npm install --save @xen-orchestra/log
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/mixin):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/mixin
|
||||
```
|
||||
> npm install --save @xen-orchestra/mixin
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/mixins):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/mixins
|
||||
```
|
||||
> npm install --save @xen-orchestra/mixins
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -25,7 +25,7 @@ For safety reasons, the proxy requires authentication to be used.
|
||||
|
||||
Use the authentication token:
|
||||
|
||||
```console
|
||||
```
|
||||
$ cat ~/.config/xo-proxy/config.z-auto.json
|
||||
{"authenticationToken":"J0BgKritQgPxoyZrBJ5ViafQfLk06YoyFwC3fmfO5wU"}
|
||||
```
|
||||
@@ -48,7 +48,7 @@ https://user:password@xo.company.lan
|
||||
|
||||
Or create a dedicated token with `xo-cli`:
|
||||
|
||||
```console
|
||||
```
|
||||
$ xo-cli --createToken xoa.company.lan admin@admin.net
|
||||
Password: ********
|
||||
Successfully logged with admin@admin.net
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/openflow):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/openflow
|
||||
```
|
||||
> npm install --save @xen-orchestra/openflow
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/proxy-cli):
|
||||
|
||||
```sh
|
||||
npm install --global @xen-orchestra/proxy-cli
|
||||
```
|
||||
> npm install --global @xen-orchestra/proxy-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -67,44 +67,38 @@ ${pkg.name} v${pkg.version}`
|
||||
// sequence path of the current call
|
||||
const callPath = []
|
||||
|
||||
let url
|
||||
let { token } = opts
|
||||
if (opts.url !== '') {
|
||||
url = new URL(opts.url)
|
||||
const { username } = url
|
||||
if (username !== '') {
|
||||
token = username
|
||||
url.username = ''
|
||||
}
|
||||
} else {
|
||||
url = new URL('https://localhost/')
|
||||
if (opts.host !== '') {
|
||||
url.host = opts.host
|
||||
} else {
|
||||
const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {}
|
||||
url.hostname = hostname
|
||||
url.port = port
|
||||
}
|
||||
}
|
||||
|
||||
url = new URL('/api/v1', url)
|
||||
const baseRequest = {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
cookie: `authenticationToken=${token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
pathname: '/api/v1',
|
||||
rejectUnauthorized: false,
|
||||
}
|
||||
let { token } = opts
|
||||
if (opts.url !== '') {
|
||||
const { protocol, host, username } = new URL(opts.url)
|
||||
Object.assign(baseRequest, { protocol, host })
|
||||
if (username !== '') {
|
||||
token = username
|
||||
}
|
||||
} else {
|
||||
baseRequest.protocol = 'https:'
|
||||
if (opts.host !== '') {
|
||||
baseRequest.host = opts.host
|
||||
} else {
|
||||
const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {}
|
||||
baseRequest.hostname = hostname
|
||||
baseRequest.port = port
|
||||
}
|
||||
}
|
||||
baseRequest.headers.cookie = `authenticationToken=${token}`
|
||||
|
||||
const call = async ({ method, params }) => {
|
||||
if (callPath.length !== 0) {
|
||||
process.stderr.write(`\n${colors.bold(`--- call #${callPath.join('.')}`)} ---\n\n`)
|
||||
}
|
||||
|
||||
const response = await hrp(url, {
|
||||
...baseRequest,
|
||||
|
||||
const response = await hrp.post(baseRequest, {
|
||||
body: format.request(0, method, params),
|
||||
})
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"content-type": "^1.0.4",
|
||||
"cson-parser": "^4.0.7",
|
||||
"getopts": "^2.2.3",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"pumpify": "^2.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/proxy",
|
||||
"version": "0.26.13",
|
||||
"version": "0.26.10",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "XO Proxy used to remotely execute backup jobs",
|
||||
"keywords": [
|
||||
@@ -32,7 +32,7 @@
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.4",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.6",
|
||||
"@xen-orchestra/backups": "^0.29.5",
|
||||
"@xen-orchestra/fs": "^3.3.1",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@xen-orchestra/mixin": "^0.1.0",
|
||||
@@ -46,7 +46,7 @@
|
||||
"get-stream": "^6.0.0",
|
||||
"getopts": "^2.2.3",
|
||||
"golike-defer": "^0.5.1",
|
||||
"http-server-plus": "^1.0.0",
|
||||
"http-server-plus": "^0.12.0",
|
||||
"http2-proxy": "^5.0.53",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"jsonrpc-websocket-client": "^0.7.2",
|
||||
@@ -60,7 +60,7 @@
|
||||
"source-map-support": "^0.5.16",
|
||||
"stoppable": "^1.0.6",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.2.5",
|
||||
"xen-api": "^1.2.2",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/self-signed):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/self-signed
|
||||
```
|
||||
> npm install --save @xen-orchestra/self-signed
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/template):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/template
|
||||
```
|
||||
> npm install --save @xen-orchestra/template
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pretty-ms": "^7.0.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"pw": "^0.0.4",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-vmdk-to-vhd": "^2.5.3"
|
||||
"xo-vmdk-to-vhd": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -230,15 +230,10 @@ export async function upload(args) {
|
||||
)
|
||||
formData.append('file', input, { filename: 'file', knownLength: length })
|
||||
try {
|
||||
const response = await hrp(url.toString(), { body: formData, headers: formData.getHeaders(), method: 'POST' })
|
||||
return await response.text()
|
||||
return await hrp.post(url.toString(), { body: formData, headers: formData.getHeaders() }).readAll('utf-8')
|
||||
} catch (e) {
|
||||
console.log('ERROR', e)
|
||||
const { response } = e
|
||||
if (response !== undefined) {
|
||||
console.log('ERROR content', await response.text())
|
||||
}
|
||||
|
||||
console.log('ERROR content', await e.response.readAll('utf-8'))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,16 +88,35 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
|
||||
async readHeaderAndFooter() {
|
||||
const buffer = await this.#read(0, 2048)
|
||||
strictEqual(buffer.readBigInt64LE(0), 0xcafebaben)
|
||||
for (let i = 0; i < 2048 / 8; i++) {
|
||||
console.log(i, '> ', buffer.readBigInt64LE(8 * i).toString(16), buffer.readBigInt64LE(8 * i))
|
||||
}
|
||||
|
||||
strictEqual(readInt64(buffer, 1), 0x200000001) // version 2.1
|
||||
|
||||
const capacity = readInt64(buffer, 2)
|
||||
const grain_size = readInt64(buffer, 3)
|
||||
const grain_table_size = readInt64(buffer, 4)
|
||||
const flags = readInt64(buffer, 5)
|
||||
|
||||
const grain_dir_offset = readInt64(buffer, 16)
|
||||
const grain_dir_size = readInt64(buffer, 17)
|
||||
const grain_tables_offset = readInt64(buffer, 18)
|
||||
const grain_tables_size = readInt64(buffer, 19)
|
||||
this.#grainOffset = readInt64(buffer, 24)
|
||||
|
||||
console.log({
|
||||
capacity,
|
||||
grain_size,
|
||||
grain_table_size,
|
||||
flags,
|
||||
grain_dir_offset,
|
||||
grain_dir_size,
|
||||
grain_tables_offset,
|
||||
grain_tables_size,
|
||||
grainSize: this.#grainSize,
|
||||
})
|
||||
|
||||
this.#grainSize = grain_size * 512 // 8 sectors / 4KB default
|
||||
this.#grainTableOffset = grain_tables_offset * 512
|
||||
this.#grainTableSize = grain_tables_size * 512
|
||||
@@ -112,10 +131,12 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
|
||||
}
|
||||
|
||||
async readBlockAllocationTable() {
|
||||
console.log('READ BLOCK ALLOCATION', this.#grainTableSize)
|
||||
const CHUNK_SIZE = 64 * 512
|
||||
|
||||
strictEqual(this.#grainTableSize % CHUNK_SIZE, 0)
|
||||
|
||||
console.log(' will read ', this.#grainTableSize / CHUNK_SIZE, 'table')
|
||||
for (let chunkIndex = 0, grainIndex = 0; chunkIndex < this.#grainTableSize / CHUNK_SIZE; chunkIndex++) {
|
||||
process.stdin.write('.')
|
||||
const start = chunkIndex * CHUNK_SIZE + this.#grainTableOffset
|
||||
@@ -130,11 +151,15 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
|
||||
break
|
||||
}
|
||||
if (entry > 3n) {
|
||||
const intIndex = +(((entry & 0x0fff000000000000n) >> 48n) | ((entry & 0x0000ffffffffffffn) << 12n))
|
||||
let pos = intIndex * this.#grainSize + CHUNK_SIZE * chunkIndex + this.#grainOffset
|
||||
console.log({ indexInChunk, grainIndex, intIndex, pos })
|
||||
this.#grainMap.set(grainIndex)
|
||||
grainIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('found', this.#grainMap.size)
|
||||
|
||||
// read grain directory and the grain tables
|
||||
const nbBlocks = this.header.maxTableEntries
|
||||
@@ -164,7 +189,7 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
|
||||
strictEqual(graintable.length, 4096 * 4)
|
||||
// we have no guaranty that data are order or contiguous
|
||||
// let's construct ranges to limit the number of queries
|
||||
let rangeStart, offsetStart, offsetEnd
|
||||
let rangeStart, offsetStart, offsetEnd, lastOffset
|
||||
|
||||
const changeRange = async (index, offset) => {
|
||||
if (offsetStart !== undefined) {
|
||||
|
||||
16
@xen-orchestra/vmware-explorer/index.mjs
Normal file
16
@xen-orchestra/vmware-explorer/index.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import Esxi from './esxi.mjs'
|
||||
const host = '10.10.0.62'
|
||||
const user = 'root'
|
||||
const password = 'vateslab'
|
||||
const sslVerify = false
|
||||
|
||||
console.log(Esxi)
|
||||
const esxi = new Esxi(host, user, password, sslVerify)
|
||||
console.log(esxi)
|
||||
esxi.on('ready', async function () {
|
||||
//const metadata = await esxi.getTransferableVmMetadata('4')
|
||||
//console.log('metadata', metadata)
|
||||
|
||||
const res = await esxi.powerOn(9)
|
||||
console.log(res)
|
||||
})
|
||||
@@ -12,7 +12,7 @@
|
||||
"vhd-lib": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=16"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/vmware-explorer",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function parseVmsd(text) {
|
||||
// remove the " around value
|
||||
set(parsed, key.split('.'), val?.substring(1, val.length - 1))
|
||||
})
|
||||
if (parsed.snapshot?.current === undefined) {
|
||||
if (parsed.snapshot?.current == undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/xapi):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/xapi
|
||||
```
|
||||
> npm install --save @xen-orchestra/xapi
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -18,7 +18,7 @@ The feature is opt-in via a tag on the VM: `xo:notify-on-snapshot`.
|
||||
|
||||
By default, it will be an HTTPS request on the port `1727`, on the first IP address reported by the VM.
|
||||
|
||||
If the _VM Tools_ (i.e. management agent) are not installed on the VM or if you wish to use another URL, you can specify this in the tag: `xo:notify-on-snapshot=<URL>`.
|
||||
If the _VM Tools_ (i.e. management agent) are not installed on the VM or if you which to use another URL, you can specify this in the tag: `xo:notify-on-snapshot=<URL>`.
|
||||
|
||||
To guarantee the request comes from XO, a secret must be provided in the `xo-server`'s (and `xo-proxy` if relevant) configuration:
|
||||
|
||||
@@ -27,20 +27,11 @@ To guarantee the request comes from XO, a secret must be provided in the `xo-ser
|
||||
syncHookSecret = 'unique long string to ensure the request comes from XO'
|
||||
```
|
||||
|
||||
XO will waits for the request to be answered before starting the snapshot, but will not wait longer than _1 minute_ by default. This timeout can be changed in the configuration as well:
|
||||
|
||||
```toml
|
||||
[xapiOptions]
|
||||
|
||||
# Timeout in milliseconds
|
||||
#
|
||||
# Default: 60e3
|
||||
syncHookTimeout = 300e3 # 5 minutes
|
||||
```
|
||||
|
||||
## Specification
|
||||
|
||||
If the request fails for any reasons (including the timeout described in the above section), XO will go ahead with snapshot immediately.
|
||||
XO will waits for the request to be answered before starting the snapshot, but will not wait longer than _1 minute_.
|
||||
|
||||
If the request fails for any reason, XO will go ahead with snapshot immediately.
|
||||
|
||||
```http
|
||||
GET /sync HTTP/1.1
|
||||
@@ -152,7 +143,7 @@ main().catch(console.warn)
|
||||
|
||||
You can run it manually for testing:
|
||||
|
||||
```console
|
||||
$ node index.cjs
|
||||
```
|
||||
> node index.cjs
|
||||
Server is listening on https://[::]:1727
|
||||
```
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xen-api": "^1.2.5"
|
||||
"xen-api": "^1.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
@@ -26,7 +26,7 @@
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"json-rpc-protocol": "^0.13.2",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,44 +1,5 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.79.2** (2023-02-20)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
### Bug fixes
|
||||
|
||||
- [Disk import] Fixes ` Cannot read properties of null (reading "length")` error
|
||||
- [Continuous Replication] Work-around _premature close_ error
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api 1.2.5
|
||||
- @xen-orchestra/proxy 0.26.13
|
||||
- xo-server 5.109.3
|
||||
|
||||
## **5.79.1** (2023-02-17)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Continuous Replication] Fix `VDI_IO_ERROR` when after a VDI has been resized
|
||||
- [REST API] Fix VDI import
|
||||
- Fix failing imports (REST API and web UI) [Forum#58146](https://xcp-ng.org/forum/post/58146)
|
||||
- [Pool/License] Fix license expiration on license binding modal (PR [#6666](https://github.com/vatesfr/xen-orchestra/pull/6666))
|
||||
- [NBD Backup] Fix VDI not disconnecting from control domain (PR [#6660](https://github.com/vatesfr/xen-orchestra/pull/6660))
|
||||
- [NBD Backup] Improve performance by avoid unnecessary VDI transfers
|
||||
- [Home/Pool] Do not check for support on non `XCP-ng` pool (PR [#6661](https://github.com/vatesfr/xen-orchestra/pull/6661))
|
||||
- [VMDK/OVA import] Fix error importing a VMDK or an OVA generated from XO (PR [#6669](https://github.com/vatesfr/xen-orchestra/pull/6669))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api 1.2.4
|
||||
- @vates/nbd-client 1.0.1
|
||||
- @xen-orchestra/backups 0.29.6
|
||||
- @xen-orchestra/proxy 0.26.12
|
||||
- xo-vmdk-to-vhd 2.5.3
|
||||
- xo-cli 0.14.4
|
||||
- xo-server 5.109.2
|
||||
- xo-server-transport-email 0.6.1
|
||||
- xo-web 5.111.1
|
||||
|
||||
## **5.79.0** (2023-01-31)
|
||||
|
||||
### Highlights
|
||||
@@ -97,7 +58,7 @@
|
||||
|
||||
## **5.78.0** (2022-12-20)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -121,6 +82,8 @@
|
||||
|
||||
## **5.77.2** (2022-12-12)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backups] Fixes most of the _unexpected number of entries in backup cache_ errors
|
||||
|
||||
@@ -27,6 +27,4 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- xo-cli minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM ubuntu:xenial
|
||||
# https://qastack.fr/programming/25899912/how-to-install-nvm-in-docker
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y curl qemu-utils vmdk-stream-converter git libxml2-utils libfuse2 nbdkit
|
||||
RUN apt-get install -y curl qemu-utils blktap-utils vmdk-stream-converter git libxml2-utils libfuse2 nbdkit
|
||||
ENV NVM_DIR /usr/local/nvm
|
||||
RUN mkdir -p /usr/local/nvm
|
||||
RUN cd /usr/local/nvm
|
||||
|
||||
@@ -134,8 +134,8 @@ This CLI is mainly used as a debug tool, there's no 100% guarantee on its stabil
|
||||
|
||||
### Usage
|
||||
|
||||
```console
|
||||
$ xo-cli --help
|
||||
```
|
||||
> xo-cli --help
|
||||
Usage:
|
||||
|
||||
xo-cli --register [--allowUnauthorized] [--expiresIn duration] <XO-Server URL> <username> [<password>]
|
||||
@@ -177,8 +177,8 @@ Usage:
|
||||
|
||||
#### Register your XO instance
|
||||
|
||||
```console
|
||||
$ xo-cli --register http://xo.my-company.net admin@admin.net admin
|
||||
```
|
||||
> xo-cli --register http://xo.my-company.net admin@admin.net admin
|
||||
Successfully logged with admin@admin.net
|
||||
```
|
||||
|
||||
@@ -188,27 +188,27 @@ Note: only a token will be saved in the configuration file.
|
||||
|
||||
Prints all objects:
|
||||
|
||||
```sh
|
||||
xo-cli --list-objects
|
||||
```
|
||||
> xo-cli --list-objects
|
||||
```
|
||||
|
||||
It is possible to filter on object properties, for instance to print
|
||||
all VM templates:
|
||||
|
||||
```sh
|
||||
xo-cli --list-objects type=VM-template
|
||||
```
|
||||
> xo-cli --list-objects type=VM-template
|
||||
```
|
||||
|
||||
#### List available commands
|
||||
|
||||
```sh
|
||||
xo-cli --list-commands
|
||||
```
|
||||
> xo-cli --list-commands
|
||||
```
|
||||
|
||||
Commands can be filtered using patterns:
|
||||
|
||||
```sh
|
||||
xo-cli --list-commands '{user,group}.*'
|
||||
```
|
||||
> xo-cli --list-commands '{user,group}.*'
|
||||
```
|
||||
|
||||
#### Execute a command
|
||||
@@ -217,8 +217,8 @@ The same syntax is used for all commands: `xo-cli <command> <param name>=<value>
|
||||
|
||||
E.g., adding a new server:
|
||||
|
||||
```console
|
||||
$ xo-cli server.add host=my.server.net username=root password=secret-password
|
||||
```
|
||||
> xo-cli server.add host=my.server.net username=root password=secret-password
|
||||
42
|
||||
```
|
||||
|
||||
@@ -228,20 +228,20 @@ Parameters (except `true` and `false` which are correctly parsed as
|
||||
booleans) are assumed to be strings. For other types, you may use JSON
|
||||
encoding by prefixing with `json:`:
|
||||
|
||||
```sh
|
||||
xo-cli foo.bar baz='json:[1, 2, 3]'
|
||||
```
|
||||
> xo-cli foo.bar baz='json:[1, 2, 3]'
|
||||
```
|
||||
|
||||
##### VM export
|
||||
|
||||
```sh
|
||||
xo-cli vm.export vm=a01667e0-8e29-49fc-a550-17be4226783c @=vm.xva
|
||||
```
|
||||
> xo-cli vm.export vm=a01667e0-8e29-49fc-a550-17be4226783c @=vm.xva
|
||||
```
|
||||
|
||||
##### VM import
|
||||
|
||||
```sh
|
||||
xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
```
|
||||
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
```
|
||||
|
||||
> Note: `xo-cli` only supports the import of XVA files. It will not import OVA files. To import OVA images, you must use the XOA web UI or use `xo-upload-ova` [available here](https://github.com/vatesfr/xen-orchestra/blob/master/@xen-orchestra/upload-ova/README.md#xo-upload-ova).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user