Compare commits
1 Commits
npm-turbo
...
lite/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be7801903 |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache: 'yarn'
|
||||
- name: Install project dependencies
|
||||
run: npm ci
|
||||
run: yarn
|
||||
- name: Build the project
|
||||
run: npm run build
|
||||
run: yarn build
|
||||
- name: Lint tests
|
||||
run: npm run test-lint
|
||||
run: yarn test-lint
|
||||
- name: Integration tests
|
||||
run: sudo npm run test-integration
|
||||
run: sudo yarn test-integration
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import LRU from 'lru-cache'
|
||||
import Fuse from 'fuse-native'
|
||||
import { VhdSynthetic } from 'vhd-lib'
|
||||
import { Disposable, fromCallback } from 'promise-toolbox'
|
||||
'use strict'
|
||||
|
||||
const LRU = require('lru-cache')
|
||||
const Fuse = require('fuse-native')
|
||||
const { VhdSynthetic } = require('vhd-lib')
|
||||
const { Disposable, fromCallback } = require('promise-toolbox')
|
||||
|
||||
// build a s stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
|
||||
const stat = st => ({
|
||||
@@ -14,7 +16,7 @@ const stat = st => ({
|
||||
gid: st.gid !== undefined ? st.gid : process.getgid(),
|
||||
})
|
||||
|
||||
export const mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
|
||||
exports.mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
|
||||
const vhd = yield VhdSynthetic.fromVhdChain(handler, diskPath)
|
||||
|
||||
const cache = new LRU({
|
||||
@@ -15,9 +15,8 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=10.0"
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"dependencies": {
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
|
||||
42
@vates/nbd-client/constants.js
Normal file
42
@vates/nbd-client/constants.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict'
|
||||
exports.INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
|
||||
exports.OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
|
||||
exports.NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
|
||||
exports.NBD_OPT_EXPORT_NAME = 1
|
||||
exports.NBD_OPT_ABORT = 2
|
||||
exports.NBD_OPT_LIST = 3
|
||||
exports.NBD_OPT_STARTTLS = 5
|
||||
exports.NBD_OPT_INFO = 6
|
||||
exports.NBD_OPT_GO = 7
|
||||
|
||||
exports.NBD_FLAG_HAS_FLAGS = 1 << 0
|
||||
exports.NBD_FLAG_READ_ONLY = 1 << 1
|
||||
exports.NBD_FLAG_SEND_FLUSH = 1 << 2
|
||||
exports.NBD_FLAG_SEND_FUA = 1 << 3
|
||||
exports.NBD_FLAG_ROTATIONAL = 1 << 4
|
||||
exports.NBD_FLAG_SEND_TRIM = 1 << 5
|
||||
|
||||
exports.NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
|
||||
|
||||
exports.NBD_CMD_FLAG_FUA = 1 << 0
|
||||
exports.NBD_CMD_FLAG_NO_HOLE = 1 << 1
|
||||
exports.NBD_CMD_FLAG_DF = 1 << 2
|
||||
exports.NBD_CMD_FLAG_REQ_ONE = 1 << 3
|
||||
exports.NBD_CMD_FLAG_FAST_ZERO = 1 << 4
|
||||
|
||||
exports.NBD_CMD_READ = 0
|
||||
exports.NBD_CMD_WRITE = 1
|
||||
exports.NBD_CMD_DISC = 2
|
||||
exports.NBD_CMD_FLUSH = 3
|
||||
exports.NBD_CMD_TRIM = 4
|
||||
exports.NBD_CMD_CACHE = 5
|
||||
exports.NBD_CMD_WRITE_ZEROES = 6
|
||||
exports.NBD_CMD_BLOCK_STATUS = 7
|
||||
exports.NBD_CMD_RESIZE = 8
|
||||
|
||||
exports.NBD_REQUEST_MAGIC = 0x25609513 // magic number to create a new NBD request to send to the server
|
||||
exports.NBD_REPLY_MAGIC = 0x67446698 // magic number received from the server when reading response to a nbd request
|
||||
exports.NBD_REPLY_ACK = 1
|
||||
|
||||
exports.NBD_DEFAULT_PORT = 10809
|
||||
exports.NBD_DEFAULT_BLOCK_SIZE = 64 * 1024
|
||||
@@ -1,41 +0,0 @@
|
||||
export const INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
|
||||
export const OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
|
||||
export const NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
|
||||
export const NBD_OPT_EXPORT_NAME = 1
|
||||
export const NBD_OPT_ABORT = 2
|
||||
export const NBD_OPT_LIST = 3
|
||||
export const NBD_OPT_STARTTLS = 5
|
||||
export const NBD_OPT_INFO = 6
|
||||
export const NBD_OPT_GO = 7
|
||||
|
||||
export const NBD_FLAG_HAS_FLAGS = 1 << 0
|
||||
export const NBD_FLAG_READ_ONLY = 1 << 1
|
||||
export const NBD_FLAG_SEND_FLUSH = 1 << 2
|
||||
export const NBD_FLAG_SEND_FUA = 1 << 3
|
||||
export const NBD_FLAG_ROTATIONAL = 1 << 4
|
||||
export const NBD_FLAG_SEND_TRIM = 1 << 5
|
||||
|
||||
export const NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
|
||||
|
||||
export const NBD_CMD_FLAG_FUA = 1 << 0
|
||||
export const NBD_CMD_FLAG_NO_HOLE = 1 << 1
|
||||
export const NBD_CMD_FLAG_DF = 1 << 2
|
||||
export const NBD_CMD_FLAG_REQ_ONE = 1 << 3
|
||||
export const NBD_CMD_FLAG_FAST_ZERO = 1 << 4
|
||||
|
||||
export const NBD_CMD_READ = 0
|
||||
export const NBD_CMD_WRITE = 1
|
||||
export const NBD_CMD_DISC = 2
|
||||
export const NBD_CMD_FLUSH = 3
|
||||
export const NBD_CMD_TRIM = 4
|
||||
export const NBD_CMD_CACHE = 5
|
||||
export const NBD_CMD_WRITE_ZEROES = 6
|
||||
export const NBD_CMD_BLOCK_STATUS = 7
|
||||
export const NBD_CMD_RESIZE = 8
|
||||
|
||||
export const NBD_REQUEST_MAGIC = 0x25609513 // magic number to create a new NBD request to send to the server
|
||||
export const NBD_REPLY_MAGIC = 0x67446698 // magic number received from the server when reading response to a nbd request
|
||||
export const NBD_REPLY_ACK = 1
|
||||
|
||||
export const NBD_DEFAULT_PORT = 10809
|
||||
export const NBD_DEFAULT_BLOCK_SIZE = 64 * 1024
|
||||
@@ -1,11 +1,8 @@
|
||||
import assert from 'node:assert'
|
||||
import { Socket } from 'node:net'
|
||||
import { connect } from 'node:tls'
|
||||
import { fromCallback, pRetry, pDelay, pTimeout } from 'promise-toolbox'
|
||||
import { readChunkStrict } from '@vates/read-chunk'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
|
||||
import {
|
||||
'use strict'
|
||||
const assert = require('node:assert')
|
||||
const { Socket } = require('node:net')
|
||||
const { connect } = require('node:tls')
|
||||
const {
|
||||
INIT_PASSWD,
|
||||
NBD_CMD_READ,
|
||||
NBD_DEFAULT_BLOCK_SIZE,
|
||||
@@ -20,13 +17,16 @@ import {
|
||||
NBD_REQUEST_MAGIC,
|
||||
OPTS_MAGIC,
|
||||
NBD_CMD_DISC,
|
||||
} from './constants.mjs'
|
||||
} = require('./constants.js')
|
||||
const { fromCallback, pRetry, pDelay, pTimeout } = require('promise-toolbox')
|
||||
const { readChunkStrict } = require('@vates/read-chunk')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
|
||||
const { warn } = createLogger('vates:nbd-client')
|
||||
|
||||
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
|
||||
|
||||
export default class NbdClient {
|
||||
module.exports = class NbdClient {
|
||||
#serverAddress
|
||||
#serverCert
|
||||
#serverPort
|
||||
@@ -17,7 +17,6 @@
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"dependencies": {
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/read-chunk": "^1.1.1",
|
||||
@@ -32,6 +31,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.mjs"
|
||||
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import NbdClient from '../index.mjs'
|
||||
import { spawn, exec } from 'node:child_process'
|
||||
import fs from 'node:fs/promises'
|
||||
import { test } from 'tap'
|
||||
import tmp from 'tmp'
|
||||
import { pFromCallback } from 'promise-toolbox'
|
||||
import { Socket } from 'node:net'
|
||||
import { NBD_DEFAULT_PORT } from '../constants.mjs'
|
||||
import assert from 'node:assert'
|
||||
'use strict'
|
||||
const NbdClient = require('../index.js')
|
||||
const { spawn, exec } = require('node:child_process')
|
||||
const fs = require('node:fs/promises')
|
||||
const { test } = require('tap')
|
||||
const tmp = require('tmp')
|
||||
const { pFromCallback } = require('promise-toolbox')
|
||||
const { Socket } = require('node:net')
|
||||
const { NBD_DEFAULT_PORT } = require('../constants.js')
|
||||
const assert = require('node:assert')
|
||||
|
||||
const FILE_SIZE = 10 * 1024 * 1024
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use strict'
|
||||
/*
|
||||
|
||||
node-vsphere-soap
|
||||
@@ -11,18 +12,17 @@
|
||||
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events'
|
||||
import axios from 'axios'
|
||||
import https from 'node:https'
|
||||
import util from 'util'
|
||||
import soap from 'soap'
|
||||
import Cookie from 'soap-cookie' // required for session persistence
|
||||
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const axios = require('axios')
|
||||
const https = require('node:https')
|
||||
const util = require('util')
|
||||
const soap = require('soap')
|
||||
const Cookie = require('soap-cookie') // required for session persistence
|
||||
// Client class
|
||||
// inherits from EventEmitter
|
||||
// possible events: connect, error, ready
|
||||
|
||||
export function Client(vCenterHostname, username, password, sslVerify) {
|
||||
function Client(vCenterHostname, username, password, sslVerify) {
|
||||
this.status = 'disconnected'
|
||||
this.reconnectCount = 0
|
||||
|
||||
@@ -228,3 +228,4 @@ function _soapErrorHandler(self, emitter, command, args, err) {
|
||||
}
|
||||
|
||||
// end
|
||||
exports.Client = Client
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@vates/node-vsphere-soap",
|
||||
"version": "1.0.0",
|
||||
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
|
||||
"main": "lib/client.mjs",
|
||||
"main": "lib/client.js",
|
||||
"author": "reedog117",
|
||||
"repository": {
|
||||
"directory": "@vates/node-vsphere-soap",
|
||||
@@ -30,7 +30,7 @@
|
||||
"private": false,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/node-vsphere-soap",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
'use strict'
|
||||
|
||||
// place your own credentials here for a vCenter or ESXi server
|
||||
// this information will be used for connecting to a vCenter instance
|
||||
// for module testing
|
||||
// name the file config-test.js
|
||||
|
||||
export const vCenterTestCreds = {
|
||||
const vCenterTestCreds = {
|
||||
vCenterIP: 'vcsa',
|
||||
vCenterUser: 'vcuser',
|
||||
vCenterPassword: 'vcpw',
|
||||
vCenter: true,
|
||||
}
|
||||
|
||||
exports.vCenterTestCreds = vCenterTestCreds
|
||||
@@ -1,16 +1,18 @@
|
||||
'use strict'
|
||||
|
||||
/*
|
||||
vsphere-soap.test.js
|
||||
|
||||
tests for the vCenterConnectionInstance class
|
||||
*/
|
||||
|
||||
import assert from 'assert'
|
||||
import { describe, it } from 'test'
|
||||
const assert = require('assert')
|
||||
const { describe, it } = require('test')
|
||||
|
||||
import * as vc from '../lib/client.mjs'
|
||||
const vc = require('../lib/client')
|
||||
|
||||
// eslint-disable-next-line n/no-missing-import
|
||||
import { vCenterTestCreds as TestCreds } from '../config-test.mjs'
|
||||
// eslint-disable-next-line n/no-missing-require
|
||||
const TestCreds = require('../config-test.js').vCenterTestCreds
|
||||
|
||||
const VItest = new vc.Client(TestCreds.vCenterIP, TestCreds.vCenterUser, TestCreds.vCenterPassword, false)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const isUtf8 = require('isutf8')
|
||||
|
||||
/**
|
||||
* Read a chunk of data from a stream.
|
||||
@@ -82,13 +81,6 @@ exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
|
||||
if (size !== undefined && chunk.length !== size) {
|
||||
const error = new Error(`stream has ended with not enough data (actual: ${chunk.length}, expected: ${size})`)
|
||||
|
||||
// Buffer.isUtf8 is too recent for now
|
||||
// @todo : replace external package by Buffer.isUtf8 when the supported version of node reach 18
|
||||
|
||||
if (chunk.length < 1024 && isUtf8(chunk)) {
|
||||
error.text = chunk.toString('utf8')
|
||||
}
|
||||
Object.defineProperties(error, {
|
||||
chunk: {
|
||||
value: chunk,
|
||||
|
||||
@@ -102,37 +102,12 @@ describe('readChunkStrict', function () {
|
||||
assert.strictEqual(error.chunk, undefined)
|
||||
})
|
||||
|
||||
it('throws if stream ends with not enough data, utf8', async () => {
|
||||
it('throws if stream ends with not enough data', async () => {
|
||||
const error = await rejectionOf(readChunkStrict(makeStream(['foo', 'bar']), 10))
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 6, expected: 10)')
|
||||
assert.strictEqual(error.text, 'foobar')
|
||||
assert.deepEqual(error.chunk, Buffer.from('foobar'))
|
||||
})
|
||||
|
||||
it('throws if stream ends with not enough data, non utf8 ', async () => {
|
||||
const source = [Buffer.alloc(10, 128), Buffer.alloc(10, 128)]
|
||||
const error = await rejectionOf(readChunkStrict(makeStream(source), 30))
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 20, expected: 30)')
|
||||
assert.strictEqual(error.text, undefined)
|
||||
assert.deepEqual(error.chunk, Buffer.concat(source))
|
||||
})
|
||||
|
||||
it('throws if stream ends with not enough data, utf8 , long data', async () => {
|
||||
const source = Buffer.from('a'.repeat(1500))
|
||||
const error = await rejectionOf(readChunkStrict(makeStream([source]), 2000))
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, `stream has ended with not enough data (actual: 1500, expected: 2000)`)
|
||||
assert.strictEqual(error.text, undefined)
|
||||
assert.deepEqual(error.chunk, source)
|
||||
})
|
||||
|
||||
it('succeed', async () => {
|
||||
const source = Buffer.from('a'.repeat(20))
|
||||
const chunk = await readChunkStrict(makeStream([source]), 10)
|
||||
assert.deepEqual(source.subarray(10), chunk)
|
||||
})
|
||||
})
|
||||
|
||||
describe('skip', function () {
|
||||
@@ -159,16 +134,6 @@ describe('skip', function () {
|
||||
it('returns less size if stream ends', async () => {
|
||||
assert.deepEqual(await skip(makeStream('foo bar'), 10), 7)
|
||||
})
|
||||
|
||||
it('put back if it read too much', async () => {
|
||||
let source = makeStream(['foo', 'bar'])
|
||||
await skip(source, 1) // read part of data chunk
|
||||
const chunk = (await readChunkStrict(source, 2)).toString('utf-8')
|
||||
assert.strictEqual(chunk, 'oo')
|
||||
|
||||
source = makeStream(['foo', 'bar'])
|
||||
assert.strictEqual(await skip(source, 3), 3) // read aligned with data chunk
|
||||
})
|
||||
})
|
||||
|
||||
describe('skipStrict', function () {
|
||||
@@ -179,9 +144,4 @@ describe('skipStrict', function () {
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 7, expected: 10)')
|
||||
assert.deepEqual(error.bytesSkipped, 7)
|
||||
})
|
||||
it('succeed', async () => {
|
||||
const source = makeStream(['foo', 'bar', 'baz'])
|
||||
const res = await skipStrict(source, 4)
|
||||
assert.strictEqual(res, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,8 +33,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"isutf8": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,10 +61,10 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run clean",
|
||||
"prepublishOnly": "npm run build",
|
||||
"pretest": "npm run build",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"pretest": "yarn run build",
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test ./dist/"
|
||||
},
|
||||
|
||||
@@ -209,7 +209,7 @@ describe('encryption', () => {
|
||||
// encrypt with a non default algorithm
|
||||
const encryptor = _getEncryptor('aes-256-cbc', '73c1838d7d8a6088ca2317fb5f29cd91')
|
||||
|
||||
await fs.writeFile(`${dir}/encryption.json`, `{"algorithm": "aes-256-gcm"}`)
|
||||
await fs.writeFile(`${dir}/encryption.json`, `{"algorithm": "aes-256-gmc"}`)
|
||||
await fs.writeFile(`${dir}/metadata.json`, encryptor.encryptData(`{"random": "NOTSORANDOM"}`))
|
||||
|
||||
// remote is now non empty : can't modify key anymore
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
- Clone
|
||||
- Copy `.env.dist` to `.env` and set vars
|
||||
- `npm ci`
|
||||
- `npm run dev`
|
||||
- `yarn`
|
||||
- `yarn dev`
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "GIT_HEAD=$(git rev-parse HEAD) vite build",
|
||||
"deploy": "./scripts/deploy.sh",
|
||||
"test": "npm run type-check",
|
||||
"test": "yarn run type-check",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,6 +20,7 @@
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@vueform/multiselect": "^2.6.2",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"complex-matcher": "^0.7.0",
|
||||
|
||||
@@ -14,8 +14,8 @@ SERVER="www-xo.gpn.vates.fr"
|
||||
|
||||
echo "Building XO Lite"
|
||||
|
||||
(cd ../.. && npm ci)
|
||||
npm run build-only --base="$BASE"
|
||||
(cd ../.. && yarn)
|
||||
yarn build-only --base="$BASE"
|
||||
|
||||
echo "Deploying XO Lite from $DIST"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "reset.css";
|
||||
@import "theme.css";
|
||||
@import "multi-select.css";
|
||||
/* TODO Serve fonts locally */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;0,900;1,400;1,500;1,600;1,700;1,900&display=swap");
|
||||
|
||||
|
||||
164
@xen-orchestra/lite/src/assets/multi-select.css
Normal file
164
@xen-orchestra/lite/src/assets/multi-select.css
Normal file
@@ -0,0 +1,164 @@
|
||||
@import "@vueform/multiselect/themes/default.css";
|
||||
|
||||
:root {
|
||||
--ms-font-size: 0.8em;
|
||||
--ms-line-height: 1.375;
|
||||
--ms-bg: var(--background-color-primary);
|
||||
--ms-bg-disabled: var(--background-color-secondary);
|
||||
--ms-border-color: var(--color-blue-scale-400);
|
||||
--ms-border-width: 0.1rem;
|
||||
--ms-border-color-active: var(--color-extra-blue-base);
|
||||
--ms-border-width-active: 0.1rem;
|
||||
--ms-radius: 0.4em;
|
||||
--ms-py: 1.08em;
|
||||
--ms-px: 0.625em;
|
||||
--ms-ring-width: 0;
|
||||
--ms-ring-color: transparent;
|
||||
--ms-placeholder-color: var(--color-blue-scale-100);
|
||||
--ms-max-height: 35rem;
|
||||
|
||||
--ms-spinner-color: var(--color-green-infra-base);
|
||||
--ms-caret-color: var(--color-blue-scale-300);
|
||||
--ms-clear-color: var(--color-blue-scale-300);
|
||||
--ms-clear-color-hover: var(--color-blue-scale-100);
|
||||
|
||||
--ms-tag-font-size: 1em;
|
||||
--ms-tag-line-height: 150%;
|
||||
--ms-tag-font-weight: 400;
|
||||
--ms-tag-bg: var(--background-color-secondary);
|
||||
--ms-tag-bg-disabled: var(--color-grayscale-200);
|
||||
--ms-tag-color: var(--color-blue-scale-200);
|
||||
--ms-tag-color-disabled: var(--color-blue-scale-500);
|
||||
--ms-tag-radius: 0.4em;
|
||||
--ms-tag-py: 0.4rem;
|
||||
--ms-tag-px: 1.2rem;
|
||||
--ms-tag-my: 0.25rem;
|
||||
--ms-tag-mx: 0.5rem;
|
||||
|
||||
--ms-tag-remove-radius: 4rem;
|
||||
--ms-tag-remove-py: 0.5rem;
|
||||
--ms-tag-remove-px: 0.5rem;
|
||||
--ms-tag-remove-my: 0rem;
|
||||
--ms-tag-remove-mx: 0.5rem;
|
||||
|
||||
--ms-dropdown-bg: var(--background-color-primary);
|
||||
--ms-dropdown-border-color: var(--color-extra-blue-base);
|
||||
--ms-dropdown-border-width: 0.1rem;
|
||||
--ms-dropdown-radius: 0.8rem;
|
||||
|
||||
--ms-group-label-py: 0.5rem;
|
||||
--ms-group-label-px: 2rem;
|
||||
--ms-group-label-line-height: 1.375;
|
||||
--ms-group-label-bg: var(--background-color-secondary);
|
||||
--ms-group-label-color: var(--color-blue-scale-100);
|
||||
--ms-group-label-bg-pointed: var(--color-blue-scale-400);
|
||||
--ms-group-label-color-pointed: var(--color-blue-scale-200);
|
||||
--ms-group-label-bg-disabled: var(--color-blue-scale-200);
|
||||
--ms-group-label-color-disabled: var(--color-blue-scale-500);
|
||||
--ms-group-label-bg-selected: var(--color-green-infra-base);
|
||||
--ms-group-label-color-selected: var(--color-blue-scale-500);
|
||||
--ms-group-label-bg-selected-pointed: var(--color-green-infra-d20);
|
||||
--ms-group-label-color-selected-pointed: var(--color-blue-scale-500);
|
||||
--ms-group-label-bg-selected-disabled: var(--color-blue-scale-200);
|
||||
--ms-group-label-color-selected-disabled: var(--color-green-infra-base);
|
||||
|
||||
--ms-option-font-size: 1em;
|
||||
--ms-option-line-height: 1.375;
|
||||
--ms-option-bg-pointed: var(--background-color-secondary);
|
||||
--ms-option-color-pointed: var(--color-blue-scale-200);
|
||||
--ms-option-bg-selected: var(--background-color-primary);
|
||||
--ms-option-color-selected: var(--color-green-infra-base);
|
||||
--ms-option-bg-disabled: var(--background-color-primary);
|
||||
--ms-option-color-disabled: var(--color-blue-scale-400);
|
||||
--ms-option-bg-selected-pointed: var(--background-color-secondary);
|
||||
--ms-option-color-selected-pointed: var(--color-green-infra-base);
|
||||
--ms-option-bg-selected-disabled: var(--background-color-primary);
|
||||
--ms-option-color-selected-disabled: var(--color-blue-scale-300);
|
||||
--ms-option-py: 1rem;
|
||||
--ms-option-px: 2rem;
|
||||
|
||||
--ms-empty-color: var(--color-grayscale-200);
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
min-width: 15rem;
|
||||
box-shadow: var(--shadow-100);
|
||||
|
||||
&:not(.is-disabled) {
|
||||
&.color-info {
|
||||
--ms-border-color: var(--color-blue-scale-400);
|
||||
--ms-border-color-active: var(--color-extra-blue-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-extra-blue-l60);
|
||||
}
|
||||
}
|
||||
|
||||
&.color-success {
|
||||
--ms-border-color: var(--color-green-infra-base);
|
||||
--ms-border-color-active: var(--color-green-infra-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-green-infra-l60);
|
||||
}
|
||||
}
|
||||
|
||||
&.color-warning {
|
||||
--ms-border-color: var(--color-orange-world-base);
|
||||
--ms-border-color-active: var(--color-orange-world-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-orange-world-l60);
|
||||
}
|
||||
}
|
||||
|
||||
&.color-error {
|
||||
--ms-border-color: var(--color-red-vates-base);
|
||||
--ms-border-color-active: var(--color-red-vates-l40);
|
||||
|
||||
&:hover {
|
||||
--ms-border-color: var(--color-red-vates-l60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .multiselect-group-label {
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
& .caret-icon {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin: 0 var(--ms-px, 0.875rem) 0 0;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
transform: rotateX(0deg);
|
||||
z-index: 10;
|
||||
color: var(--ms-caret-color);
|
||||
}
|
||||
|
||||
&.is-open .caret-icon {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
& .multiselect-tag-remove {
|
||||
color: var(--background-color-secondary);
|
||||
background-color: var(--color-blue-scale-200);
|
||||
}
|
||||
|
||||
& .multiselect-tag-remove:hover {
|
||||
background-color: var(--color-red-vates-l40);
|
||||
}
|
||||
|
||||
& .multiselect-tag-remove-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
& .multiselect-search,
|
||||
& .multiselect-tags-search {
|
||||
color: var(--color-blue-scale-100);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,15 @@ import {
|
||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import { uniqueId, upperFirst } from "lodash-es";
|
||||
import { computed, reactive, ref, watch, watchEffect } from "vue";
|
||||
import {
|
||||
computed,
|
||||
effectScope,
|
||||
onBeforeUnmount,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const tab = (tab: TAB, params: Param[]) =>
|
||||
@@ -178,6 +186,24 @@ if (propParams.value.length !== 0) {
|
||||
}
|
||||
|
||||
const propValues = ref<Record<string, any>>({});
|
||||
|
||||
const scope = effectScope();
|
||||
|
||||
scope.run(() => {
|
||||
for (const param of props.params) {
|
||||
if (!isPropParam(param) || !param.hasChangeHandler()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => propValues.value[param.name],
|
||||
(value) => param.getOnChangeHandler()?.(value, propValues.value)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => scope.stop());
|
||||
|
||||
const settingValues = ref<Record<string, any>>({});
|
||||
const eventsLog = ref<
|
||||
{ id: string; name: string; args: { name: string; value: any }[] }[]
|
||||
@@ -236,8 +262,12 @@ const eventLogRows = computed(() => {
|
||||
const slotProperties = computed(() => {
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
propParams.value.forEach(({ name }) => {
|
||||
properties[name] = propValues.value[name];
|
||||
propParams.value.forEach((param) => {
|
||||
const value = propValues.value[param.name];
|
||||
|
||||
if (param.isRequired() || value !== undefined) {
|
||||
properties[param.name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
eventParams.value.forEach((eventParam) => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<th><!-- Reset Default --></th>
|
||||
<th><!-- Widget --></th>
|
||||
<th>Default</th>
|
||||
<th>Help</th>
|
||||
<th><!-- Help --></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
@@ -78,7 +78,11 @@
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td class="help">
|
||||
{{ param.getHelp() }}
|
||||
<UiIcon
|
||||
v-if="param.getHelp()"
|
||||
v-tooltip="param.getHelp()"
|
||||
:icon="faInfoCircle"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -95,7 +99,11 @@ import useModal from "@/composables/modal.composable";
|
||||
import useSortedCollection from "@/composables/sorted-collection.composable";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import type { PropParam } from "@/libs/story/story-param";
|
||||
import { faClose, faRepeat } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faClose,
|
||||
faInfoCircle,
|
||||
faRepeat,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { toRef } from "vue";
|
||||
|
||||
@@ -168,6 +176,7 @@ const {
|
||||
.help {
|
||||
font-style: italic;
|
||||
color: var(--color-blue-scale-200);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.default-value {
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
<template>
|
||||
<FormSelect
|
||||
v-if="isSelectWidget(widget)"
|
||||
v-model="model"
|
||||
:wrapper-attrs="{ class: 'full-width' }"
|
||||
>
|
||||
<option v-if="!required && model === undefined" :value="undefined" />
|
||||
<option
|
||||
v-for="choice in widget.choices"
|
||||
:key="choice.label"
|
||||
:value="choice.value"
|
||||
>
|
||||
{{ choice.label }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<div v-else-if="isRadioWidget(widget)" class="radio">
|
||||
<FormInputWrapper v-for="choice in widget.choices" :key="choice.label">
|
||||
<FormRadio v-model="model" :value="choice.value" />
|
||||
{{ choice.label }}
|
||||
</FormInputWrapper>
|
||||
<div class="story-widget">
|
||||
<div v-if="isSelectWidget(widget)">
|
||||
<FormSelect :options="widget.choices" v-model="model" />
|
||||
</div>
|
||||
<div v-else-if="isRadioWidget(widget)" class="radio">
|
||||
<FormInputWrapper v-for="choice in widget.choices" :key="choice.label">
|
||||
<FormRadio v-model="model" :value="choice.value" />
|
||||
{{ choice.label }}
|
||||
</FormInputWrapper>
|
||||
</div>
|
||||
<div v-else-if="isBooleanWidget(widget)">
|
||||
<FormCheckbox v-model="model" />
|
||||
</div>
|
||||
<FormInput
|
||||
v-else-if="isNumberWidget(widget)"
|
||||
v-model.number="model"
|
||||
type="number"
|
||||
/>
|
||||
<FormInput v-else-if="isTextWidget(widget)" v-model="model" />
|
||||
<FormJson v-else-if="isObjectWidget(widget)" v-model="model" />
|
||||
</div>
|
||||
<div v-else-if="isBooleanWidget(widget)">
|
||||
<FormCheckbox v-model="model" />
|
||||
</div>
|
||||
<FormInput
|
||||
v-else-if="isNumberWidget(widget)"
|
||||
v-model.number="model"
|
||||
type="number"
|
||||
/>
|
||||
<FormInput v-else-if="isTextWidget(widget)" v-model="model" />
|
||||
<FormJson v-else-if="isObjectWidget(widget)" v-model="model" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -82,9 +73,11 @@ const model = useVModel(props, "modelValue", emit);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-input,
|
||||
.form-json {
|
||||
font-size: 1.4rem;
|
||||
.story-widget {
|
||||
&:deep(.form-select),
|
||||
&:deep(.form-input),
|
||||
&:deep(.form-json) {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -157,7 +157,7 @@ defineExpose({
|
||||
max-width: 30em;
|
||||
|
||||
--before-width: v-bind('beforeWidth || "1.75em"');
|
||||
--after-width: v-bind('afterWidth || "1.625em"');
|
||||
--after-width: v-bind('afterWidth || "1.75em"');
|
||||
--caret-width: 1.5em;
|
||||
|
||||
--text-color: var(--color-blue-scale-100);
|
||||
@@ -187,9 +187,9 @@ defineExpose({
|
||||
.input,
|
||||
.textarea,
|
||||
.select {
|
||||
font-size: 1em;
|
||||
font-size: 0.8em;
|
||||
width: 100%;
|
||||
height: 3em;
|
||||
height: 3.5em;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
border: 0.05em solid var(--border-color);
|
||||
@@ -292,11 +292,11 @@ defineExpose({
|
||||
padding-left: 0.625em;
|
||||
|
||||
&.has-before {
|
||||
padding-left: calc(var(--before-width) + 0.25em);
|
||||
padding-left: calc(var(--before-width) + 0.6em);
|
||||
}
|
||||
|
||||
&.has-after {
|
||||
padding-right: calc(var(--after-width) + 0.25em);
|
||||
padding-right: calc(var(--after-width) + 0.6em);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
:slotted(.form-input),
|
||||
:slotted(.form-select) {
|
||||
&:deep(.form-input),
|
||||
&:deep(.form-select) {
|
||||
&:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -23,7 +23,8 @@
|
||||
margin-left: -1px;
|
||||
|
||||
.input,
|
||||
.select {
|
||||
.select,
|
||||
.multiselect {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
@@ -31,7 +32,8 @@
|
||||
|
||||
&:not(:last-child) {
|
||||
.input,
|
||||
.select {
|
||||
.select,
|
||||
.multiselect {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ import type { Color } from "@/types";
|
||||
import {
|
||||
IK_FORM_HAS_LABEL,
|
||||
IK_FORM_INPUT_COLOR,
|
||||
IK_FORM_LABEL_DISABLED,
|
||||
IK_INPUT_ID,
|
||||
} from "@/types/injection-keys";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
@@ -59,7 +58,6 @@ const props = defineProps<{
|
||||
warning?: string;
|
||||
error?: string;
|
||||
help?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const id = computed(() => props.id ?? uniqueId("form-input-"));
|
||||
@@ -83,11 +81,6 @@ provide(
|
||||
IK_FORM_HAS_LABEL,
|
||||
computed(() => slots.label !== undefined)
|
||||
);
|
||||
|
||||
provide(
|
||||
IK_FORM_LABEL_DISABLED,
|
||||
computed(() => props.disabled ?? false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -1,15 +1,93 @@
|
||||
<template>
|
||||
<FormInput>
|
||||
<slot />
|
||||
</FormInput>
|
||||
<span class="form-select">
|
||||
<MultiSelect
|
||||
v-model="modelValue"
|
||||
:can-clear="clearable"
|
||||
:class="colorClass"
|
||||
:close-on-deselect="!multiple"
|
||||
:close-on-select="!multiple"
|
||||
:groups="isGrouped"
|
||||
:hide-selected="false"
|
||||
:label="labelKey"
|
||||
:mode="multiple ? 'multiple' : 'single'"
|
||||
:multiple-label="getMultipleLabel"
|
||||
:no-options-text="$t('no-options-available')"
|
||||
:no-results-text="$t('no-results-found')"
|
||||
:options="options"
|
||||
:track-by="labelKey"
|
||||
:value-prop="valueKey"
|
||||
:object="object"
|
||||
:disabled="busy || disabled"
|
||||
:searchable="options.length > SEARCHABLE_THRESHOLD"
|
||||
:loading="busy"
|
||||
>
|
||||
<template #caret>
|
||||
<UiIcon :icon="faAngleDown" class="caret-icon" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import { IK_INPUT_TYPE } from "@/types/injection-keys";
|
||||
import { provide } from "vue";
|
||||
<script generic="T extends XenApiRecord<string>" lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import type { XenApiRecord } from "@/libs/xen-api";
|
||||
import type { Color } from "@/types";
|
||||
import { IK_FORM_INPUT_COLOR } from "@/types/injection-keys";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import MultiSelect from "@vueform/multiselect";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { computed, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
provide(IK_INPUT_TYPE, "select");
|
||||
const SEARCHABLE_THRESHOLD = 10;
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: any;
|
||||
multiple?: boolean;
|
||||
options: { label: string; options: T[] }[] | T[];
|
||||
labelKey?: string;
|
||||
valueKey?: string;
|
||||
clearable?: boolean;
|
||||
color?: Color;
|
||||
object?: boolean;
|
||||
disabled?: boolean;
|
||||
busy?: boolean;
|
||||
}>(),
|
||||
{
|
||||
labelKey: "label",
|
||||
valueKey: "value",
|
||||
color: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: any): void;
|
||||
}>();
|
||||
|
||||
const isGrouped = computed(() => {
|
||||
const option = props.options[0];
|
||||
return "object" === typeof option && "options" in option && "label" in option;
|
||||
});
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const getMultipleLabel = (values: any[]) =>
|
||||
i18n.t("n-options-selected", { n: values.length });
|
||||
|
||||
const parentColor = inject(IK_FORM_INPUT_COLOR, undefined);
|
||||
|
||||
const colorClass = computed(() => {
|
||||
const color = props.color ?? parentColor?.value ?? "info";
|
||||
|
||||
return `color-${color}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
<style lang="postcss" scoped>
|
||||
.form-select {
|
||||
font-size: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
82
@xen-orchestra/lite/src/components/form/FormTag.vue
Normal file
82
@xen-orchestra/lite/src/components/form/FormTag.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<span class="form-tag">
|
||||
<MultiSelect
|
||||
v-model="modelValue"
|
||||
:class="colorClass"
|
||||
:create-option="allowNew"
|
||||
:no-options-text="$t('no-options-available')"
|
||||
:no-results-text="$t('no-results-found')"
|
||||
:on-create="handleCreate"
|
||||
:options="options"
|
||||
:searchable="allowNew || options.length > SEARCHABLE_THRESHOLD"
|
||||
mode="tags"
|
||||
@deselect="($event) => handleDeselect($event as string)"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #caret>
|
||||
<UiIcon :icon="faAngleDown" class="caret-icon" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import type { Color } from "@/types";
|
||||
import { IK_FORM_INPUT_COLOR } from "@/types/injection-keys";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import MultiSelect from "@vueform/multiselect";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { computed, inject } from "vue";
|
||||
|
||||
const SEARCHABLE_THRESHOLD = 10;
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: any;
|
||||
createdTags?: string[];
|
||||
options: string[];
|
||||
allowNew?: boolean;
|
||||
color?: Color;
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{ createdTags: () => [] }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: string[]): void;
|
||||
(event: "update:createdTags", value: string[]): void;
|
||||
}>();
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit);
|
||||
|
||||
const handleCreate = (tag: { label: string; value: string }) => {
|
||||
emit("update:createdTags", [...props.createdTags, tag.value]);
|
||||
return tag;
|
||||
};
|
||||
|
||||
const handleDeselect = (value: string) => {
|
||||
if (!props.allowNew) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
"update:createdTags",
|
||||
props.createdTags.filter((t) => t !== value)
|
||||
);
|
||||
};
|
||||
|
||||
const parentColor = inject(IK_FORM_INPUT_COLOR, undefined);
|
||||
|
||||
const colorClass = computed(() => {
|
||||
const color = props.color ?? parentColor?.value ?? "info";
|
||||
|
||||
return `color-${color}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.form-tag {
|
||||
font-size: 2rem;
|
||||
}
|
||||
</style>
|
||||
31
@xen-orchestra/lite/src/components/form/FormXapiRecord.vue
Normal file
31
@xen-orchestra/lite/src/components/form/FormXapiRecord.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<FormSelect
|
||||
object
|
||||
v-model="modelValue"
|
||||
:color="color"
|
||||
:multiple="multiple"
|
||||
:options="options"
|
||||
label-key="name_label"
|
||||
value-key="$ref"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script generic="T extends XenApiRecord<string>" lang="ts" setup>
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import type { XenApiRecord } from "@/libs/xen-api";
|
||||
import type { Color } from "@/types";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
multiple?: boolean;
|
||||
options: { label: string; options: T[] }[] | T[];
|
||||
color?: Color;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: any): void;
|
||||
}>();
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit);
|
||||
</script>
|
||||
@@ -102,12 +102,26 @@ export class PropParam extends mixin(BaseParam, WithWidget, WithType) {
|
||||
#isRequired = false;
|
||||
#defaultValue: any;
|
||||
#isVModel: boolean;
|
||||
#onChangeHandler: ((value: any, context: object) => void) | undefined;
|
||||
|
||||
constructor(name: string, isVModel = false) {
|
||||
super(name);
|
||||
this.#isVModel = isVModel;
|
||||
}
|
||||
|
||||
onChange(handler: (value: any, context: object) => void) {
|
||||
this.#onChangeHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
hasChangeHandler() {
|
||||
return this.#onChangeHandler !== undefined;
|
||||
}
|
||||
|
||||
getOnChangeHandler() {
|
||||
return this.#onChangeHandler;
|
||||
}
|
||||
|
||||
isRequired() {
|
||||
return this.#isRequired;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"log-out": "Log out",
|
||||
"login": "Login",
|
||||
"migrate": "Migrate",
|
||||
"n-options-selected": "{n} option selected | {n} options selected",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Name",
|
||||
"network": "Network",
|
||||
@@ -77,6 +78,8 @@
|
||||
"news": "News",
|
||||
"news-name": "{name} news",
|
||||
"new-features-are-coming": "New features are coming soon!",
|
||||
"no-options-available": "No options available",
|
||||
"no-results-found": "No results found",
|
||||
"object": "Object",
|
||||
"object-not-found": "Object {id} can't be found…",
|
||||
"or": "Or",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"log-out": "Se déconnecter",
|
||||
"login": "Connexion",
|
||||
"migrate": "Migrer",
|
||||
"n-options-selected": "{n} option sélectionnée | {n} options sélectionnées",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Nom",
|
||||
"network": "Réseau",
|
||||
@@ -77,6 +78,8 @@
|
||||
"news": "Actualités",
|
||||
"news-name": "Actualités {name}",
|
||||
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
|
||||
"no-options-available": "Aucune option disponible",
|
||||
"no-results-found": "Aucun résultat trouvé",
|
||||
"object": "Objet",
|
||||
"object-not-found": "L'objet {id} est introuvable…",
|
||||
"or": "Ou",
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
```vue-template
|
||||
<FormInputGroup>
|
||||
<FormInput />
|
||||
<FormInput />
|
||||
<FormSelect>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
<option>Option 3</option>
|
||||
</FormSelect>
|
||||
<FormInput ... />
|
||||
<FormInput ... />
|
||||
<FormSelect ... />
|
||||
</FormInputGroup>
|
||||
```
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
:params="[slot().help('Can contains multiple FormInput and FormSelect')]"
|
||||
>
|
||||
<FormInputGroup>
|
||||
<FormInput />
|
||||
<FormInput />
|
||||
<FormSelect>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
<option>Option 3</option>
|
||||
</FormSelect>
|
||||
<FormInput v-model="model" />
|
||||
<FormInput v-model="model" />
|
||||
<FormSelect
|
||||
v-model="model"
|
||||
:options="['Option 1', 'Option 2', 'Option 3']"
|
||||
/>
|
||||
</FormInputGroup>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
@@ -20,4 +19,7 @@ import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormInputGroup from "@/components/form/FormInputGroup.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import { slot } from "@/libs/story/story-param";
|
||||
import { ref } from "vue";
|
||||
|
||||
const model = ref("");
|
||||
</script>
|
||||
|
||||
72
@xen-orchestra/lite/src/stories/form-select.story.md
Normal file
72
@xen-orchestra/lite/src/stories/form-select.story.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `options` prop
|
||||
|
||||
## Array of strings
|
||||
|
||||
```ts
|
||||
const options = ["Option 1", "Option 2", "Option 3"];
|
||||
```
|
||||
|
||||
## Array of objects
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
];
|
||||
```
|
||||
|
||||
### Custom properties
|
||||
|
||||
When not using `label` and `value` properties, you can change them with `label-key` and `value-key` props.
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{ name: "Option 1", id: "option1" },
|
||||
{ name: "Option 2", id: "option2" },
|
||||
{ name: "Option 3", id: "option3" },
|
||||
];
|
||||
```
|
||||
|
||||
```html
|
||||
<FormSelect :options="options" label-key="name" value-key="id" />
|
||||
```
|
||||
|
||||
## Array of groups
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{
|
||||
label: "Group 1",
|
||||
options: [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Group 2",
|
||||
options: [
|
||||
{ label: "Option 4", value: "option4" },
|
||||
{ label: "Option 5", value: "option5" },
|
||||
{ label: "Option 6", value: "option6" },
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
# `object` prop
|
||||
|
||||
```ts
|
||||
const options = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
];
|
||||
```
|
||||
|
||||
By default, when selection "Option 2", the value sent to `v-model` will be `option2`.
|
||||
|
||||
If you want to send the whole object, you can use `object` prop.
|
||||
|
||||
In this case, the value sent to `v-model` will be `{ label: 'Option 2', value: 'option2' }`.
|
||||
60
@xen-orchestra/lite/src/stories/form-select.story.vue
Normal file
60
@xen-orchestra/lite/src/stories/form-select.story.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().required().type('any'),
|
||||
prop('options').required().arr().preset(options),
|
||||
prop('multiple').bool().widget().onChange(handleMultipleChange),
|
||||
prop('labelKey')
|
||||
.default('label')
|
||||
.str()
|
||||
.help(
|
||||
'If `options` is an array of objects, item label will be extracted from this key'
|
||||
),
|
||||
prop('valueKey')
|
||||
.default('value')
|
||||
.str()
|
||||
.help(
|
||||
'If `options` is an array of objects, item value will be extracted from this key'
|
||||
),
|
||||
prop('clearable')
|
||||
.bool()
|
||||
.widget()
|
||||
.help('When true, adds a clear button on the right side of the select'),
|
||||
colorProp(),
|
||||
prop('disabled').bool().widget(),
|
||||
prop('object')
|
||||
.bool()
|
||||
.widget()
|
||||
.help(
|
||||
'If `options` is an array of objects, the whole object will be selected instead of only the value'
|
||||
),
|
||||
]"
|
||||
>
|
||||
<FormSelect v-if="isActive" v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import { colorProp, model, prop } from "@/libs/story/story-param";
|
||||
import { nextTick, ref } from "vue";
|
||||
|
||||
const options = [
|
||||
{ label: "Option 1", value: "1" },
|
||||
{ label: "Option 2", value: "2" },
|
||||
{ label: "Option 3", value: "3" },
|
||||
];
|
||||
|
||||
// Workaround to prevent errors when `multiple` changes
|
||||
const isActive = ref(true);
|
||||
|
||||
const handleMultipleChange = (isMultiple, context) => {
|
||||
isActive.value = false;
|
||||
context.modelValue = isMultiple ? [] : null;
|
||||
nextTick(() => {
|
||||
isActive.value = true;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
31
@xen-orchestra/lite/src/stories/form-tag.story.vue
Normal file
31
@xen-orchestra/lite/src/stories/form-tag.story.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model()
|
||||
.required()
|
||||
.type('string[]')
|
||||
.help('List of selected tags (including created ones)'),
|
||||
model('createdTags').type('string[]').help('List of created tags'),
|
||||
prop('options')
|
||||
.required()
|
||||
.arr('string')
|
||||
.preset(availableTags)
|
||||
.help('List of available tags')
|
||||
.widget(object()),
|
||||
prop('allowNew').bool().help('Allow to create new tags').widget(),
|
||||
colorProp(),
|
||||
]"
|
||||
>
|
||||
<FormTag v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import FormTag from "@/components/form/FormTag.vue";
|
||||
import { colorProp, model, prop } from "@/libs/story/story-param";
|
||||
import { object } from "@/libs/story/story-widget";
|
||||
|
||||
const availableTags = ["First tag", "Second tag", "Third tag"];
|
||||
</script>
|
||||
@@ -0,0 +1,6 @@
|
||||
```typescript
|
||||
type XenApiRecordGroup = {
|
||||
label: string;
|
||||
options: XenApiRecord[];
|
||||
}[];
|
||||
```
|
||||
58
@xen-orchestra/lite/src/stories/form-xapi-record.story.vue
Normal file
58
@xen-orchestra/lite/src/stories/form-xapi-record.story.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().type('XenApiRecord').required(),
|
||||
prop('multiple').bool().widget().onChange(handleMultipleChange),
|
||||
prop('options')
|
||||
.required()
|
||||
.arr()
|
||||
.type('XenApiRecord[] | XenApiRecordGroup[]')
|
||||
.widget()
|
||||
.preset(options),
|
||||
colorProp(),
|
||||
prop('disabled').bool().widget(),
|
||||
]"
|
||||
>
|
||||
<FormXapiRecord v-if="isActive" v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import FormXapiRecord from "@/components/form/FormXapiRecord.vue";
|
||||
import { colorProp, model, prop } from "@/libs/story/story-param";
|
||||
import { nextTick, ref } from "vue";
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "ISOs - Storage Lab",
|
||||
options: [
|
||||
{
|
||||
$ref: "1",
|
||||
name_label: "AlmaLinux-8.3-x86_64-minimal.iso",
|
||||
},
|
||||
{
|
||||
$ref: "2",
|
||||
name_label: "AlmaLinux-8.5-x86_64-boot.iso",
|
||||
},
|
||||
{ $ref: "3", name_label: "CentOS-6.10-i386-minimal.iso" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "XCP-ng Tools - XO Lab",
|
||||
options: [{ $ref: "4", name_label: "guest-tools.iso" }],
|
||||
},
|
||||
];
|
||||
|
||||
// Workaround to prevent errors when `multiple` is changed
|
||||
const isActive = ref(true);
|
||||
|
||||
const handleMultipleChange = (isMultiple, context) => {
|
||||
isActive.value = false;
|
||||
context.modelValue = isMultiple ? [] : null;
|
||||
nextTick(() => {
|
||||
isActive.value = true;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -4,12 +4,7 @@
|
||||
|
||||
<div class="row">
|
||||
Choose a component
|
||||
<FormSelect v-model="componentPath">
|
||||
<option value="" />
|
||||
<option v-for="path in componentPaths" :key="path">
|
||||
{{ path }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<FormSelect v-model="componentPath" :options="componentPaths" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -101,7 +101,7 @@ configure(transportConsole())
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> npm add nodemailer pretty-format
|
||||
> yarn add nodemailer pretty-format
|
||||
```
|
||||
|
||||
Configuration:
|
||||
@@ -127,7 +127,7 @@ configure(
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> npm add split-host syslog-client
|
||||
> yarn add split-host syslog-client
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
@@ -119,7 +119,7 @@ configure(transportConsole())
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> npm add nodemailer pretty-format
|
||||
> yarn add nodemailer pretty-format
|
||||
```
|
||||
|
||||
Configuration:
|
||||
@@ -145,7 +145,7 @@ configure(
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> npm add split-host syslog-client
|
||||
> yarn add split-host syslog-client
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"postversion": "npm publish --access public",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.4",
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true,
|
||||
"author": {
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class VhdEsxiCowd extends VhdAbstract {
|
||||
|
||||
// depending on the paramters we also look into the parent data
|
||||
return (
|
||||
this.#grainDirectory.readUInt32LE(blockId * 4) !== 0 ||
|
||||
this.#grainDirectory.readInt32LE(blockId * 4) !== 0 ||
|
||||
(this.#lookMissingBlockInParent && this.#parentVhd.containsBlock(blockId))
|
||||
)
|
||||
}
|
||||
@@ -61,14 +61,14 @@ export default class VhdEsxiCowd extends VhdAbstract {
|
||||
const buffer = await this.#read(0, 2048)
|
||||
|
||||
strictEqual(buffer.slice(0, 4).toString('ascii'), 'COWD')
|
||||
strictEqual(buffer.readUInt32LE(4), 1) // version
|
||||
strictEqual(buffer.readUInt32LE(8), 3) // flags
|
||||
const numSectors = buffer.readUInt32LE(12)
|
||||
const grainSize = buffer.readUInt32LE(16)
|
||||
strictEqual(buffer.readInt32LE(4), 1) // version
|
||||
strictEqual(buffer.readInt32LE(8), 3) // flags
|
||||
const numSectors = buffer.readInt32LE(12)
|
||||
const grainSize = buffer.readInt32LE(16)
|
||||
strictEqual(grainSize, 1) // 1 grain should be 1 sector long
|
||||
strictEqual(buffer.readUInt32LE(20), 4) // grain directory position in sectors
|
||||
strictEqual(buffer.readInt32LE(20), 4) // grain directory position in sectors
|
||||
|
||||
const nbGrainDirectoryEntries = buffer.readUInt32LE(24)
|
||||
const nbGrainDirectoryEntries = buffer.readInt32LE(24)
|
||||
strictEqual(nbGrainDirectoryEntries, Math.ceil(numSectors / 4096))
|
||||
const size = numSectors * 512
|
||||
// a grain directory entry contains the address of a grain table
|
||||
@@ -90,7 +90,7 @@ export default class VhdEsxiCowd extends VhdAbstract {
|
||||
// we're lucky : a grain address can address exacty a full block
|
||||
async readBlock(blockId) {
|
||||
notEqual(this.#grainDirectory, undefined, 'grainDirectory is not loaded')
|
||||
const sectorOffset = this.#grainDirectory.readUInt32LE(blockId * 4)
|
||||
const sectorOffset = this.#grainDirectory.readInt32LE(blockId * 4)
|
||||
|
||||
const buffer = (await this.#parentVhd.readBlock(blockId)).buffer
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class VhdEsxiCowd extends VhdAbstract {
|
||||
}
|
||||
|
||||
for (let i = 0; i < graintable.length / 4; i++) {
|
||||
const grainOffset = graintable.readUInt32LE(i * 4)
|
||||
const grainOffset = graintable.readInt32LE(i * 4)
|
||||
if (grainOffset === 0) {
|
||||
// the content from parent : it is already in buffer
|
||||
await changeRange()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _computeGeometryForSize from 'vhd-lib/_computeGeometryForSize.js'
|
||||
import { createFooter, createHeader } from 'vhd-lib/_createFooterHeader.js'
|
||||
import { DISK_TYPES, FOOTER_SIZE } from 'vhd-lib/_constants.js'
|
||||
import { readChunkStrict, skipStrict } from '@vates/read-chunk'
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
import { Task } from '@vates/task'
|
||||
import { unpackFooter, unpackHeader } from 'vhd-lib/Vhd/_utils.js'
|
||||
import { VhdAbstract } from 'vhd-lib'
|
||||
@@ -21,10 +21,6 @@ export default class VhdEsxiRaw extends VhdAbstract {
|
||||
#header
|
||||
#footer
|
||||
|
||||
#streamOffset = 0
|
||||
#stream
|
||||
#reading = false
|
||||
|
||||
static async open(esxi, datastore, path, opts) {
|
||||
const vhd = new VhdEsxiRaw(esxi, datastore, path, opts)
|
||||
await vhd.readHeaderAndFooter()
|
||||
@@ -53,10 +49,10 @@ export default class VhdEsxiRaw extends VhdAbstract {
|
||||
|
||||
this.#header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
const geometry = _computeGeometryForSize(length)
|
||||
const actualSize = geometry.actualSize
|
||||
|
||||
this.#footer = unpackFooter(
|
||||
// length can be smaller than disk capacity due to alignment to head/cylinder/sector
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
|
||||
createFooter(actualSize, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,65 +64,12 @@ export default class VhdEsxiRaw extends VhdAbstract {
|
||||
return this.#bat.has(blockId)
|
||||
}
|
||||
|
||||
async #readChunk(start, length) {
|
||||
if (this.#reading) {
|
||||
throw new Error('reading must be done sequentially')
|
||||
}
|
||||
try {
|
||||
this.#reading = true
|
||||
if (this.#stream !== undefined) {
|
||||
// stream is too far ahead or to far behind
|
||||
if (this.#streamOffset > start || this.#streamOffset + VHD_BLOCK_LENGTH < start) {
|
||||
this.#stream.destroy()
|
||||
this.#stream = undefined
|
||||
this.#streamOffset = 0
|
||||
}
|
||||
}
|
||||
// no stream
|
||||
if (this.#stream === undefined) {
|
||||
const end = this.footer.currentSize - 1
|
||||
const res = await this.#esxi.download(this.#datastore, this.#path, `${start}-${end}`)
|
||||
this.#stream = res.body
|
||||
this.#streamOffset = start
|
||||
}
|
||||
|
||||
// stream a little behind
|
||||
if (this.#streamOffset < start) {
|
||||
await skipStrict(this.#stream, start - this.#streamOffset)
|
||||
this.#streamOffset = start
|
||||
}
|
||||
|
||||
// really read data
|
||||
this.#streamOffset += length
|
||||
const data = await readChunkStrict(this.#stream, length)
|
||||
return data
|
||||
} catch (error) {
|
||||
error.start = start
|
||||
error.length = length
|
||||
error.streamLength = this.footer.currentSize
|
||||
this.#stream?.destroy()
|
||||
this.#stream = undefined
|
||||
this.#streamOffset = 0
|
||||
throw error
|
||||
} finally {
|
||||
this.#reading = false
|
||||
}
|
||||
}
|
||||
|
||||
async #readBlock(blockId) {
|
||||
async readBlock(blockId) {
|
||||
const start = blockId * VHD_BLOCK_LENGTH
|
||||
let length = VHD_BLOCK_LENGTH
|
||||
let partial = false
|
||||
if (start + length > this.footer.currentSize) {
|
||||
length = this.footer.currentSize - start
|
||||
partial = true
|
||||
}
|
||||
const end = (blockId + 1) * VHD_BLOCK_LENGTH - 1
|
||||
|
||||
let data = await this.#readChunk(start, length)
|
||||
const data = await (await this.#esxi.download(this.#datastore, this.#path, `${start}-${end}`)).buffer()
|
||||
|
||||
if (partial) {
|
||||
data = Buffer.concat([data, Buffer.alloc(VHD_BLOCK_LENGTH - data.length)])
|
||||
}
|
||||
const bitmap = Buffer.alloc(512, 255)
|
||||
return {
|
||||
id: blockId,
|
||||
@@ -136,44 +79,28 @@ export default class VhdEsxiRaw extends VhdAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
async readBlock(blockId) {
|
||||
let tries = 5
|
||||
let lastError
|
||||
while (tries > 0) {
|
||||
try {
|
||||
const res = await this.#readBlock(blockId)
|
||||
return res
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
lastError.blockId = blockId
|
||||
console.warn('got error , will retry in 2seconds', lastError)
|
||||
}
|
||||
await new Promise(resolve => setTimeout(() => resolve(), 2000))
|
||||
tries--
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
// this will read all the disk once to check which block contains data, it can take a long time to execute depending on the network speed
|
||||
async readBlockAllocationTable() {
|
||||
if (!this.#thin) {
|
||||
// fast path : is we do not use thin mode, the BAT is full
|
||||
return
|
||||
}
|
||||
const res = await this.#esxi.download(this.#datastore, this.#path)
|
||||
const length = res.headers.get('content-length')
|
||||
const stream = res.body
|
||||
const empty = Buffer.alloc(VHD_BLOCK_LENGTH, 0)
|
||||
let pos = 0
|
||||
this.#bat = new Set()
|
||||
let nextChunkLength = Math.min(VHD_BLOCK_LENGTH, this.footer.currentSize)
|
||||
Task.set('total', this.footer.currentSize / VHD_BLOCK_LENGTH)
|
||||
let nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length)
|
||||
Task.set('total', length / VHD_BLOCK_LENGTH)
|
||||
const progress = setInterval(() => {
|
||||
Task.set('progress', Math.round((pos * 100) / this.footer.currentSize))
|
||||
console.log('reading blocks', pos / VHD_BLOCK_LENGTH, '/', this.footer.currentSize / VHD_BLOCK_LENGTH)
|
||||
Task.set('progress', Math.round((pos * 100) / length))
|
||||
console.log('reading blocks', pos / VHD_BLOCK_LENGTH, '/', length / VHD_BLOCK_LENGTH)
|
||||
}, 30 * 1000)
|
||||
|
||||
while (nextChunkLength > 0) {
|
||||
try {
|
||||
const chunk = await this.#readChunk(pos, nextChunkLength)
|
||||
const chunk = await readChunk(stream, nextChunkLength)
|
||||
let isEmpty
|
||||
if (nextChunkLength === VHD_BLOCK_LENGTH) {
|
||||
isEmpty = empty.equals(chunk)
|
||||
@@ -185,28 +112,15 @@ export default class VhdEsxiRaw extends VhdAbstract {
|
||||
this.#bat.add(pos / VHD_BLOCK_LENGTH)
|
||||
}
|
||||
pos += VHD_BLOCK_LENGTH
|
||||
nextChunkLength = Math.min(VHD_BLOCK_LENGTH, this.footer.currentSize - pos)
|
||||
nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length - pos)
|
||||
} catch (error) {
|
||||
clearInterval(progress)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
'BAT reading done, remaining ',
|
||||
this.#bat.size,
|
||||
'/',
|
||||
Math.ceil(this.footer.currentSize / VHD_BLOCK_LENGTH)
|
||||
)
|
||||
console.log('BAT reading done, remaining ', this.#bat.size, '/', Math.ceil(length / VHD_BLOCK_LENGTH))
|
||||
clearInterval(progress)
|
||||
}
|
||||
|
||||
rawContent() {
|
||||
return this.#esxi.download(this.#datastore, this.#path).then(res => {
|
||||
const stream = res.body
|
||||
stream.length = this.footer.currentSize
|
||||
return stream
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable no-console */
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Client } from '@vates/node-vsphere-soap'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { dirname } from 'node:path'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { strictEqual, notStrictEqual } from 'node:assert'
|
||||
@@ -10,8 +9,6 @@ import parseVmdk from './parsers/vmdk.mjs'
|
||||
import parseVmsd from './parsers/vmsd.mjs'
|
||||
import parseVmx from './parsers/vmx.mjs'
|
||||
|
||||
const { warn } = createLogger('xo:vmware-explorer:esxi')
|
||||
|
||||
export default class Esxi extends EventEmitter {
|
||||
#client
|
||||
#cookies
|
||||
@@ -67,7 +64,7 @@ export default class Esxi extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
async #download(dataStore, path, range) {
|
||||
async download(dataStore, path, range) {
|
||||
strictEqual(this.#ready, true)
|
||||
notStrictEqual(this.#dcPath, undefined)
|
||||
const url = new URL('https://localhost')
|
||||
@@ -105,24 +102,6 @@ export default class Esxi extends EventEmitter {
|
||||
return res
|
||||
}
|
||||
|
||||
async download(dataStore, path, range) {
|
||||
let tries = 5
|
||||
let lastError
|
||||
while (tries > 0) {
|
||||
try {
|
||||
const res = await this.#download(dataStore, path, range)
|
||||
return res
|
||||
} catch (error) {
|
||||
warn('got error , will retry in 2 seconds', { error })
|
||||
lastError = error
|
||||
}
|
||||
await new Promise(resolve => setTimeout(() => resolve(), 2000))
|
||||
tries--
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
// inspired from https://github.com/reedog117/node-vsphere-soap/blob/master/test/vsphere-soap.test.js#L95
|
||||
async search(type, properties) {
|
||||
// get property collector
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
"version": "0.2.3",
|
||||
"name": "@xen-orchestra/vmware-explorer",
|
||||
"dependencies": {
|
||||
"@vates/node-vsphere-soap": "^1.0.0",
|
||||
"@vates/read-chunk": "^1.1.1",
|
||||
"@vates/task": "^0.2.0",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@vates/read-chunk": "^1.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^3.3.0",
|
||||
"@vates/node-vsphere-soap": "^1.0.0",
|
||||
"vhd-lib": "^4.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
- [Incremental Backup & Replication] Attempt to work around HVM multiplier issues when creating VMs on older XAPIs (PR [#6866](https://github.com/vatesfr/xen-orchestra/pull/6866))
|
||||
- [REST API] Fix VDI export when NBD is enabled
|
||||
- [XO Config Cloud Backup] Improve wording about passphrase (PR [#6938](https://github.com/vatesfr/xen-orchestra/pull/6938))
|
||||
- [Pool] Fix IPv6 handling when adding hosts
|
||||
- [New SR] Send provided NFS version to XAPI when probing a share
|
||||
- [Backup/exports] Show more information on error ` stream has ended with not enough data (actual: xxx, expected: 512)` (PR [#6940](https://github.com/vatesfr/xen-orchestra/pull/6940))
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -36,17 +33,10 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @vates/fuse-vhd major
|
||||
- @vates/nbd-client major
|
||||
- @vates/node-vsphere-soap major
|
||||
- @xen-orchestra/backups minor
|
||||
- @xen-orchestra/vmware-explorer minor
|
||||
- @xen-orchestra/xapi major
|
||||
- @vates/read-chunk minor
|
||||
- complex-matcher patch
|
||||
- xen-api patch
|
||||
- xo-server patch
|
||||
- xo-server-transport-xmpp patch
|
||||
- xo-server-audit patch
|
||||
- xo-web minor
|
||||
|
||||
|
||||
@@ -93,6 +93,12 @@ v16.14.0
|
||||
|
||||
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.
|
||||
|
||||
#### Yarn
|
||||
|
||||
Yarn is a package manager that offers more guarantees than npm.
|
||||
|
||||
See [this page](https://yarnpkg.com/en/docs/install#debian-stable) for instructions on how to install Yarn.
|
||||
|
||||
#### Packages
|
||||
|
||||
XO needs the following packages to be installed. Redis is used as a database by XO.
|
||||
@@ -123,12 +129,12 @@ git clone -b master https://github.com/vatesfr/xen-orchestra
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
Now that you have the code, you can enter the `xen-orchestra` directory and use `npm ci` to install other dependencies. Then finally build it using `npm run build`. Be sure to run `npm` commands as the same user you will be using to run Xen Orchestra:
|
||||
Now that you have the code, you can enter the `xen-orchestra` directory and use `yarn` to install other dependencies. Then finally build it using `yarn build`. Be sure to run `yarn` commands as the same user you will be using to run Xen Orchestra:
|
||||
|
||||
```sh
|
||||
cd xen-orchestra
|
||||
npm ci
|
||||
npm run build
|
||||
yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
Now you have to create a config file for `xo-server`:
|
||||
@@ -146,7 +152,7 @@ In this config file, you can change default ports (80 and 443) for xo-server. If
|
||||
You can try to start xo-server to see if it works. You should have something like this:
|
||||
|
||||
```console
|
||||
$ npm run start
|
||||
$ yarn start
|
||||
WebServer listening on localhost:80
|
||||
[INFO] Default user: "admin@admin.net" with password "admin"
|
||||
```
|
||||
@@ -156,7 +162,7 @@ WebServer listening on localhost:80
|
||||
The only part you need to launch is xo-server, which is quite easy to do. From the `xen-orchestra/packages/xo-server` directory, run the following:
|
||||
|
||||
```sh
|
||||
npm run start
|
||||
yarn start
|
||||
```
|
||||
|
||||
That's it! Use your browser to visit the xo-server IP address, and it works! :)
|
||||
@@ -170,8 +176,8 @@ If you would like to update your current version, enter your `xen-orchestra` dir
|
||||
git checkout .
|
||||
|
||||
git pull --ff-only
|
||||
npm ci
|
||||
npm run build
|
||||
yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
Then restart Xen Orchestra if it was running.
|
||||
@@ -181,7 +187,7 @@ Then restart Xen Orchestra if it was running.
|
||||
- You can use [forever](https://github.com/nodejitsu/forever) to have the process always running:
|
||||
|
||||
```sh
|
||||
npm i -g forever
|
||||
yarn global add forever
|
||||
|
||||
# Run the below as the user owning XO
|
||||
forever start dist/cli.mjs
|
||||
@@ -190,8 +196,8 @@ forever start dist/cli.mjs
|
||||
- Or you can use [forever-service](https://github.com/zapty/forever-service) to install XO as a system service, so it starts automatically at boot. Run the following as root:
|
||||
|
||||
```sh
|
||||
npm i -g forever
|
||||
npm i -g forever-service
|
||||
yarn global add forever
|
||||
yarn global add forever-service
|
||||
|
||||
# Be sure to edit the path below to where your install is located!
|
||||
cd /home/username/xen-orchestra/packages/xo-server/
|
||||
@@ -232,8 +238,8 @@ Exceptional individual contributers are awarded with a free XOA Premium subscrip
|
||||
If you have problems during the building phase, follow these steps in your `xen-orchestra` directory:
|
||||
|
||||
1. `rm -rf node_modules`
|
||||
1. `npm ci`
|
||||
1. `npm run build`
|
||||
1. `yarn`
|
||||
1. `yarn build`
|
||||
|
||||
### FreeBSD
|
||||
|
||||
@@ -278,7 +284,7 @@ service redis start
|
||||
If you are using OpenBSD, you need to install these packages:
|
||||
|
||||
```sh
|
||||
pkg_add gmake redis python--%2.7 git node autoconf
|
||||
pkg_add gmake redis python--%2.7 git node autoconf yarn
|
||||
```
|
||||
|
||||
A few of the npm packages look for system binaries as part of their installation, and if missing will try to build it themselves. Installing these will save some time and allow for easier upgrades later:
|
||||
@@ -301,10 +307,10 @@ ulimit -n 10240
|
||||
ln -s /usr/local/bin/node /tmp/node
|
||||
```
|
||||
|
||||
If `npm` cannot find Python, give it an hand :
|
||||
If `yarn` cannot find Python, give it an hand :
|
||||
|
||||
```sh
|
||||
PYTHON=/usr/local/bin/python2 npm
|
||||
PYTHON=/usr/local/bin/python2 yarn
|
||||
```
|
||||
|
||||
Enable redis on boot with:
|
||||
|
||||
49047
package-lock.json
generated
49047
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,7 @@
|
||||
"scripts": {
|
||||
"build": "turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
|
||||
"build:xo-lite": "turbo run build --scope @xen-orchestra/lite",
|
||||
"ci": "npm ci && npm run build && npm run test-lint && npm run test-integration",
|
||||
"ci": "yarn && yarn build && yarn test-lint && yarn test-integration",
|
||||
"clean": "scripts/run-script.js --parallel clean",
|
||||
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
|
||||
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
|
||||
@@ -104,5 +104,6 @@
|
||||
"workspaces": [
|
||||
"@*/*",
|
||||
"packages/*"
|
||||
]
|
||||
],
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"plot": "gnuplot -p memory-test.gnu",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish",
|
||||
"test": "tap"
|
||||
}
|
||||
|
||||
@@ -954,8 +954,6 @@ export class Xapi extends EventEmitter {
|
||||
url,
|
||||
agent: this.httpAgent,
|
||||
})
|
||||
const { hostname } = url
|
||||
url.hostnameRaw = hostname[0] === '[' ? hostname.slice(1, -1) : hostname
|
||||
this._url = url
|
||||
}
|
||||
|
||||
|
||||
@@ -30,12 +30,14 @@ const parseResult = result => {
|
||||
return result.Value
|
||||
}
|
||||
|
||||
export default ({ secureOptions, url: { hostnameRaw, pathname, port, protocol }, agent }) => {
|
||||
const removeBrackets = hostname => (hostname[0] === '[' ? hostname.slice(1, -1) : hostname)
|
||||
|
||||
export default ({ secureOptions, url: { hostname, pathname, port, protocol }, agent }) => {
|
||||
const secure = protocol === 'https:'
|
||||
const client = (secure ? createSecureClient : createClient)({
|
||||
...(secure ? secureOptions : undefined),
|
||||
agent,
|
||||
host: hostnameRaw,
|
||||
host: removeBrackets(hostname),
|
||||
pathname,
|
||||
port,
|
||||
})
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc -w",
|
||||
"prepublishOnly": "npm run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "node dist/index.js",
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test"
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/audit-core": "^0.2.3",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true,
|
||||
"author": {
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "1.0.8",
|
||||
"engines": {
|
||||
|
||||
@@ -118,13 +118,14 @@ describe('issue', () => {
|
||||
|
||||
```
|
||||
> npm ci
|
||||
> npm run start
|
||||
> yarn start
|
||||
```
|
||||
|
||||
You get all the test suites passed (`PASS`) or failed (`FAIL`).
|
||||
|
||||
```
|
||||
> npm run test
|
||||
> yarn test
|
||||
yarn run v1.9.4
|
||||
$ jest
|
||||
PASS src/user/user.spec.js
|
||||
PASS src/job/job.spec.js
|
||||
@@ -138,11 +139,11 @@ describe('issue', () => {
|
||||
Done in 7.92s.
|
||||
```
|
||||
|
||||
- You can run only tests related to changed files, and review the failed output by using: `> npm run start -- --watch`
|
||||
- You can run only tests related to changed files, and review the failed output by using: `> yarn start --watch`
|
||||
|
||||
- ⚠ Warning: snapshots ⚠
|
||||
After each run of the tests, check that snapshots are not inadvertently modified.
|
||||
|
||||
- ⚠ Jest known issue ⚠
|
||||
If a test timeout is triggered the next async tests can fail, it's due to an inadvertently modified snapshots.
|
||||
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> npm run start -- --testTimeout=100000`
|
||||
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> yarn start --testTimeout=100000`
|
||||
|
||||
@@ -126,13 +126,14 @@ describe('issue', () => {
|
||||
|
||||
```
|
||||
> npm ci
|
||||
> npm run start
|
||||
> yarn start
|
||||
```
|
||||
|
||||
You get all the test suites passed (`PASS`) or failed (`FAIL`).
|
||||
|
||||
```
|
||||
> npm run test
|
||||
> yarn test
|
||||
yarn run v1.9.4
|
||||
$ jest
|
||||
PASS src/user/user.spec.js
|
||||
PASS src/job/job.spec.js
|
||||
@@ -146,14 +147,14 @@ describe('issue', () => {
|
||||
Done in 7.92s.
|
||||
```
|
||||
|
||||
- You can run only tests related to changed files, and review the failed output by using: `> npm run start -- --watch`
|
||||
- You can run only tests related to changed files, and review the failed output by using: `> yarn start --watch`
|
||||
|
||||
- ⚠ Warning: snapshots ⚠
|
||||
After each run of the tests, check that snapshots are not inadvertently modified.
|
||||
|
||||
- ⚠ Jest known issue ⚠
|
||||
If a test timeout is triggered the next async tests can fail, it's due to an inadvertently modified snapshots.
|
||||
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> npm run start -- --testTimeout=100000`
|
||||
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> yarn start --testTimeout=100000`
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.1.2",
|
||||
"engines": {
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xmpp/client": "^0.13.1",
|
||||
"node-xmpp-client": "^3.0.0",
|
||||
"promise-toolbox": "^0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -43,9 +43,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fromEvent from 'promise-toolbox/fromEvent'
|
||||
import { client, xml } from '@xmpp/client'
|
||||
import XmppClient from 'node-xmpp-client'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -46,16 +46,13 @@ class TransportXmppPlugin {
|
||||
this._client = null
|
||||
}
|
||||
|
||||
configure({ host, jid, port, password }) {
|
||||
this._conf = {
|
||||
password,
|
||||
service: Object.assign(new URL('xmpp://localhost'), { hostname: host, port }).href,
|
||||
username: jid,
|
||||
}
|
||||
configure(conf) {
|
||||
this._conf = conf
|
||||
this._conf.reconnect = true
|
||||
}
|
||||
|
||||
async load() {
|
||||
this._client = client(this._conf)
|
||||
this._client = new XmppClient(this._conf)
|
||||
this._client.on('error', () => {})
|
||||
|
||||
await fromEvent(this._client.connection.socket, 'data')
|
||||
@@ -74,14 +71,12 @@ class TransportXmppPlugin {
|
||||
_sendToXmppClient({ to, message }) {
|
||||
for (const receiver of to) {
|
||||
this._client.send(
|
||||
xml(
|
||||
'message',
|
||||
{
|
||||
to: receiver,
|
||||
type: 'chat',
|
||||
},
|
||||
xml('body', {}, message)
|
||||
)
|
||||
new XmppClient.Stanza('message', {
|
||||
to: receiver,
|
||||
type: 'chat',
|
||||
})
|
||||
.c('body')
|
||||
.t(message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ Manual install procedure is [available here](https://xen-orchestra.com/docs/from
|
||||
|
||||
Production build:
|
||||
|
||||
```console
|
||||
$ npm run build
|
||||
```sh
|
||||
yarn run build
|
||||
```
|
||||
|
||||
Development build:
|
||||
|
||||
```console
|
||||
$ npm run dev
|
||||
```sh
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
## How to report a bug?
|
||||
|
||||
@@ -154,11 +154,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"_build": "index-modules --index-file index.mjs src/api src/xapi/mixins src/xo-mixins && babel --delete-dir-on-start --keep-file-extension --source-maps --out-dir=dist/ src/",
|
||||
"build": "cross-env NODE_ENV=production npm run _build",
|
||||
"dev": "cross-env NODE_ENV=development npm run _build --watch",
|
||||
"prepublishOnly": "npm run build",
|
||||
"build": "cross-env NODE_ENV=production yarn run _build",
|
||||
"dev": "cross-env NODE_ENV=development yarn run _build --watch",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "node dist/cli.mjs",
|
||||
"pretest": "npm run build",
|
||||
"pretest": "yarn run build",
|
||||
"test": "tap 'dist/**/*.spec.mjs'"
|
||||
},
|
||||
"author": {
|
||||
|
||||
@@ -467,11 +467,10 @@ createZfs.resolve = {
|
||||
// This function helps to detect all NFS shares (exports) on a NFS server
|
||||
// Return a table of exports with their paths and ACLs
|
||||
|
||||
export async function probeNfs({ host, nfsVersion, server }) {
|
||||
export async function probeNfs({ host, server }) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
nfsversion: nfsVersion,
|
||||
server,
|
||||
}
|
||||
|
||||
@@ -502,7 +501,6 @@ export async function probeNfs({ host, nfsVersion, server }) {
|
||||
|
||||
probeNfs.params = {
|
||||
host: { type: 'string' },
|
||||
nfsVersion: { type: 'string', optional: true },
|
||||
server: { type: 'string' },
|
||||
}
|
||||
|
||||
@@ -839,11 +837,10 @@ probeHbaExists.resolve = {
|
||||
// This function helps to detect if this NFS SR already exists in XAPI
|
||||
// It returns a table of SR UUID, empty if no existing connections
|
||||
|
||||
export async function probeNfsExists({ host, nfsVersion, server, serverPath }) {
|
||||
export async function probeNfsExists({ host, server, serverPath }) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
nfsversion: nfsVersion,
|
||||
server,
|
||||
serverpath: serverPath,
|
||||
}
|
||||
@@ -862,7 +859,6 @@ export async function probeNfsExists({ host, nfsVersion, server, serverPath }) {
|
||||
|
||||
probeNfsExists.params = {
|
||||
host: { type: 'string' },
|
||||
nfsVersion: { type: 'string', optional: true },
|
||||
server: { type: 'string' },
|
||||
serverPath: { type: 'string' },
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import defaults from 'lodash/defaults.js'
|
||||
import findKey from 'lodash/findKey.js'
|
||||
import forEach from 'lodash/forEach.js'
|
||||
import identity from 'lodash/identity.js'
|
||||
@@ -9,7 +10,9 @@ import sum from 'lodash/sum.js'
|
||||
import uniq from 'lodash/uniq.js'
|
||||
import zipWith from 'lodash/zipWith.js'
|
||||
import { BaseError } from 'make-error'
|
||||
import { limitConcurrency } from 'limit-concurrency-decorator'
|
||||
import { parseDateTime } from '@xen-orchestra/xapi'
|
||||
import { synchronized } from 'decorator-synchronized'
|
||||
|
||||
export class FaultyGranularity extends BaseError {}
|
||||
|
||||
@@ -62,6 +65,8 @@ const computeValues = (dataRow, legendIndex, transformValue = identity) =>
|
||||
|
||||
const combineStats = (stats, path, combineValues) => zipWith(...map(stats, path), (...values) => combineValues(values))
|
||||
|
||||
const createGetProperty = (obj, property, defaultValue) => defaults(obj, { [property]: defaultValue })[property]
|
||||
|
||||
const testMetric = (test, type) =>
|
||||
typeof test === 'string' ? test === type : typeof test === 'function' ? test(type) : test.exec(type)
|
||||
|
||||
@@ -221,20 +226,31 @@ const STATS = {
|
||||
// data: Item[columns] // Item = { t: Number, values: Number[rows] }
|
||||
// }
|
||||
|
||||
// Local cache
|
||||
// _statsByObject : {
|
||||
// [uuid]: {
|
||||
// [step]: {
|
||||
// endTimestamp: Number, // the timestamp of the last statistic point
|
||||
// interval: Number, // step
|
||||
// stats: {
|
||||
// [metric1]: Number[],
|
||||
// [metric2]: {
|
||||
// [subMetric]: Number[],
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
export default class XapiStats {
|
||||
// hostCache => host uid => granularity => {
|
||||
// timestamp
|
||||
// value : promise or value
|
||||
// }
|
||||
#hostCache = {}
|
||||
constructor() {
|
||||
this._statsByObject = {}
|
||||
}
|
||||
|
||||
_updateJsonCache(xapi, host, step, timestamp) {
|
||||
const hostUuid = host.uuid
|
||||
this.#hostCache[hostUuid] = this.#hostCache[hostUuid] ?? {}
|
||||
const promise = xapi
|
||||
// Execute one http request on a XenServer for get stats
|
||||
// Return stats (Json format) or throws got exception
|
||||
@limitConcurrency(3)
|
||||
_getJson(xapi, host, timestamp, step) {
|
||||
return xapi
|
||||
.getResource('/rrd_updates', {
|
||||
host,
|
||||
query: {
|
||||
@@ -246,40 +262,27 @@ export default class XapiStats {
|
||||
},
|
||||
})
|
||||
.then(response => response.text().then(JSON5.parse))
|
||||
.catch(err => {
|
||||
delete this.#hostCache[hostUuid][step]
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
// clear cache when too old
|
||||
setTimeout(() => {
|
||||
// only if it has not been updated
|
||||
if (this.#hostCache[hostUuid]?.[step]?.timestamp === timestamp) {
|
||||
delete this.#hostCache[hostUuid][step]
|
||||
}
|
||||
}, (step + 1) * 1000)
|
||||
// To avoid multiple requests, we keep a cash for the stats and
|
||||
// only return it if we not exceed a step
|
||||
_getCachedStats(uuid, step, currentTimeStamp) {
|
||||
const statsByObject = this._statsByObject
|
||||
|
||||
this.#hostCache[hostUuid][step] = {
|
||||
timestamp,
|
||||
value: promise,
|
||||
const stats = statsByObject[uuid]?.[step]
|
||||
if (stats === undefined) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_isCacheStale(hostUuid, step, timestamp) {
|
||||
const byHost = this.#hostCache[hostUuid]?.[step]
|
||||
// cache is empty or too old
|
||||
return byHost === undefined || byHost.timestamp + step < timestamp
|
||||
}
|
||||
|
||||
// Execute one http request on a XenServer for get stats
|
||||
// Return stats (Json format) or throws got exception
|
||||
_getJson(xapi, host, timestamp, step) {
|
||||
if (this._isCacheStale(host.uuid, step, timestamp)) {
|
||||
this._updateJsonCache(xapi, host, step, timestamp)
|
||||
if (stats.endTimestamp + step < currentTimeStamp) {
|
||||
delete statsByObject[uuid][step]
|
||||
return
|
||||
}
|
||||
return this.#hostCache[host.uuid][step].value
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
@synchronized.withKey((_, { host }) => host.uuid)
|
||||
async _getAndUpdateStats(xapi, { host, uuid, granularity }) {
|
||||
const step = granularity === undefined ? RRD_STEP_SECONDS : RRD_STEP_FROM_STRING[granularity]
|
||||
|
||||
@@ -291,61 +294,65 @@ export default class XapiStats {
|
||||
|
||||
const currentTimeStamp = await getServerTimestamp(xapi, host.$ref)
|
||||
|
||||
const stats = this._getCachedStats(uuid, step, currentTimeStamp)
|
||||
if (stats !== undefined) {
|
||||
return stats
|
||||
}
|
||||
|
||||
const maxDuration = step * RRD_POINTS_PER_STEP[step]
|
||||
|
||||
// To avoid crossing over the boundary, we ask for one less step
|
||||
const optimumTimestamp = currentTimeStamp - maxDuration + step
|
||||
const json = await this._getJson(xapi, host, optimumTimestamp, step)
|
||||
const actualStep = json.meta.step
|
||||
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
}
|
||||
let stepStats
|
||||
const actualStep = json.meta.step
|
||||
if (json.data.length > 0) {
|
||||
// fetched data is organized from the newest to the oldest
|
||||
// but this implementation requires it in the other direction
|
||||
const data = [...json.data]
|
||||
data.reverse()
|
||||
json.data.reverse()
|
||||
json.meta.legend.forEach((legend, index) => {
|
||||
const [, type, uuidInStat, metricType] = /^AVERAGE:([^:]+):(.+):(.+)$/.exec(legend)
|
||||
const [, type, uuid, metricType] = /^AVERAGE:([^:]+):(.+):(.+)$/.exec(legend)
|
||||
|
||||
const metrics = STATS[type]
|
||||
if (metrics === undefined) {
|
||||
return
|
||||
}
|
||||
if (uuidInStat !== uuid) {
|
||||
return
|
||||
}
|
||||
|
||||
const { metric, testResult } = findMetric(metrics, metricType)
|
||||
if (metric === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const xoObjectStats = createGetProperty(this._statsByObject, uuid, {})
|
||||
let stepStats = xoObjectStats[actualStep]
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
|
||||
stepStats = {
|
||||
stepStats = xoObjectStats[actualStep] = {
|
||||
endTimestamp: json.meta.end,
|
||||
interval: actualStep,
|
||||
stats: {},
|
||||
}
|
||||
}
|
||||
|
||||
const path = metric.getPath !== undefined ? metric.getPath(testResult) : [findKey(metrics, metric)]
|
||||
|
||||
const lastKey = path.length - 1
|
||||
let metricStats = stepStats.stats
|
||||
let metricStats = createGetProperty(stepStats, 'stats', {})
|
||||
path.forEach((property, key) => {
|
||||
if (key === lastKey) {
|
||||
metricStats[property] = computeValues(data, index, metric.transformValue)
|
||||
metricStats[property] = computeValues(json.data, index, metric.transformValue)
|
||||
return
|
||||
}
|
||||
metricStats = metricStats[property] = metricStats[property] ?? {}
|
||||
|
||||
metricStats = createGetProperty(metricStats, property, {})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
}
|
||||
|
||||
return (
|
||||
stepStats ?? {
|
||||
this._statsByObject[uuid]?.[step] ?? {
|
||||
endTimestamp: currentTimeStamp,
|
||||
interval: step,
|
||||
stats: {},
|
||||
|
||||
@@ -24,6 +24,7 @@ import { execa } from 'execa'
|
||||
export default class BackupNgFileRestore {
|
||||
constructor(app) {
|
||||
this._app = app
|
||||
this._mounts = { __proto__: null }
|
||||
|
||||
// clean any LVM volumes that might have not been properly
|
||||
// unmounted
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fromEvent } from 'promise-toolbox'
|
||||
import { createRunner } from '@xen-orchestra/backups/Backup.mjs'
|
||||
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
|
||||
import { v4 as generateUuid } from 'uuid'
|
||||
import { VDI_FORMAT_RAW, VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
|
||||
import { VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
|
||||
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
|
||||
import Esxi from '@xen-orchestra/vmware-explorer/esxi.mjs'
|
||||
import openDeltaVmdkasVhd from '@xen-orchestra/vmware-explorer/openDeltaVmdkAsVhd.mjs'
|
||||
@@ -271,16 +271,10 @@ export default class MigrateVm {
|
||||
}
|
||||
parentVhd = vhd
|
||||
}
|
||||
// it can be empty if the VM don't have a snapshot and is running
|
||||
if (vhd !== undefined) {
|
||||
if (thin) {
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
} else {
|
||||
// no transformation when there is no snapshot in thick mode
|
||||
const stream = await vhd.rawContent()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_RAW })
|
||||
}
|
||||
// it can be empty if the VM don't have a snapshot and is running
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
}
|
||||
return { vdi, vhd }
|
||||
})
|
||||
|
||||
@@ -589,7 +589,7 @@ export default class XenServers {
|
||||
const sourceXapi = this.getXapi(sourcePoolId)
|
||||
const {
|
||||
_auth: { user, password },
|
||||
_url: { hostnameRaw },
|
||||
_url: { hostname },
|
||||
} = this.getXapi(targetPoolId)
|
||||
|
||||
// We don't want the events of the source XAPI to interfere with
|
||||
@@ -597,7 +597,7 @@ export default class XenServers {
|
||||
sourceXapi.xo.uninstall()
|
||||
|
||||
try {
|
||||
await sourceXapi.joinPool(hostnameRaw, user, password, force)
|
||||
await sourceXapi.joinPool(hostname, user, password, force)
|
||||
} catch (e) {
|
||||
sourceXapi.xo.install()
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "npm run clean",
|
||||
"predev": "npm run clean",
|
||||
"prepublishOnly": "npm run build",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
},
|
||||
"author": {
|
||||
|
||||
@@ -143,9 +143,9 @@
|
||||
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",
|
||||
"clean": "gulp clean",
|
||||
"dev": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=development gulp build",
|
||||
"prebuild": "npm run clean && index-modules --auto src",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prebuild": "yarn run clean && index-modules --auto src",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
@@ -2513,7 +2513,7 @@ const messages = {
|
||||
licensesBinding: 'Licenses binding',
|
||||
notEnoughXcpngLicenses: 'Not enough XCP-ng licenses',
|
||||
notBoundSelectLicense: 'Not bound (Plan (ID), expiration date)',
|
||||
xcpngLicensesBindingAvancedView: "To bind an XCP-ng license, go to the pool's Advanced tab.",
|
||||
xcpngLicensesBindingAvancedView: "To bind an XCP-ng license, go the pool's Advanced tab.",
|
||||
xosanUnregisteredDisclaimer:
|
||||
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
|
||||
xosanSourcesDisclaimer:
|
||||
|
||||
@@ -2693,10 +2693,9 @@ export const fetchFiles = (remote, disk, partition, paths) =>
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const probeSrNfs = (host, server, nfsVersion) => _call('sr.probeNfs', { host, nfsVersion, server })
|
||||
export const probeSrNfs = (host, server) => _call('sr.probeNfs', { host, server })
|
||||
|
||||
export const probeSrNfsExists = (host, server, serverPath, nfsVersion) =>
|
||||
_call('sr.probeNfsExists', { host, nfsVersion, server, serverPath })
|
||||
export const probeSrNfsExists = (host, server, serverPath) => _call('sr.probeNfsExists', { host, server, serverPath })
|
||||
|
||||
export const probeSrIscsiIqns = (host, target, port = undefined, chapUser = undefined, chapPassword) => {
|
||||
const params = { host, target }
|
||||
|
||||
@@ -999,7 +999,7 @@ const New = decorate([
|
||||
<Tooltip content={_('clickForMoreInformation')}>
|
||||
<a
|
||||
className='text-info'
|
||||
href='https://xen-orchestra.com/docs/incremental_backups.html#key-backup-interval'
|
||||
href='https://xen-orchestra.com/docs/delta_backups.html#full-backup-interval'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
|
||||
@@ -467,11 +467,11 @@ export default class New extends Component {
|
||||
_handleSearchServer = async () => {
|
||||
const { password, port, server, username } = this.refs
|
||||
|
||||
const { host, nfsVersion, type } = this.state
|
||||
const { host, type } = this.state
|
||||
|
||||
try {
|
||||
if (type === 'nfs' || type === 'nfsiso') {
|
||||
const paths = await probeSrNfs(host.id, server.value, nfsVersion !== '' ? nfsVersion : undefined)
|
||||
const paths = await probeSrNfs(host.id, server.value)
|
||||
this.setState({
|
||||
usage: undefined,
|
||||
paths,
|
||||
@@ -500,12 +500,12 @@ export default class New extends Component {
|
||||
|
||||
_handleSrPathSelection = async path => {
|
||||
const { server } = this.refs
|
||||
const { host, nfsVersion } = this.state
|
||||
const { host } = this.state
|
||||
|
||||
try {
|
||||
this.setState(({ loading }) => ({ loading: loading + 1 }))
|
||||
this.setState({
|
||||
existingSrs: await probeSrNfsExists(host.id, server.value, path, nfsVersion !== '' ? nfsVersion : undefined),
|
||||
existingSrs: await probeSrNfsExists(host.id, server.value, path),
|
||||
path,
|
||||
usage: true,
|
||||
summary: true,
|
||||
|
||||
@@ -24,6 +24,7 @@ esac
|
||||
if [ $# -ge 2 ]
|
||||
then
|
||||
npm --save=false --workspaces=false version "$2"
|
||||
git checkout HEAD :/yarn.lock
|
||||
fi
|
||||
# if version is not passed, simply update other packages
|
||||
|
||||
|
||||
Reference in New Issue
Block a user