Compare commits

..

1 Commits

Author SHA1 Message Date
Thierry
4be7801903 feat(lite): new multiselect component 2023-07-24 09:06:19 +02:00
97 changed files with 22625 additions and 49628 deletions

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,8 +33,5 @@
},
"devDependencies": {
"test": "^3.2.1"
},
"dependencies": {
"isutf8": "^4.0.0"
}
}

View File

@@ -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/"
},

View File

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

View File

@@ -2,8 +2,8 @@
- Clone
- Copy `.env.dist` to `.env` and set vars
- `npm ci`
- `npm run dev`
- `yarn`
- `yarn dev`
## Conventions

View File

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

View File

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

View File

@@ -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");

View 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;
}
}

View File

@@ -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) => {

View File

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

View File

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

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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' }`.

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

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

View File

@@ -0,0 +1,6 @@
```typescript
type XenApiRecordGroup = {
label: string;
options: XenApiRecord[];
}[];
```

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

@@ -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' },
}

View File

@@ -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: {},

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

@@ -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": [

View File

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

View File

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

View File

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

View File

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

View File

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

21578
yarn.lock Normal file

File diff suppressed because it is too large Load Diff