Compare commits

..

1 Commits

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

View File

@@ -1,11 +1,8 @@
'use strict'
module.exports = {
arrowParens: 'avoid',
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',
// 2020-11-24: Requested by nraynaud and approved by the rest of the team
//

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

@@ -1,6 +1,6 @@
{
"name": "@vates/fuse-vhd",
"version": "2.0.0",
"version": "1.0.0",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
@@ -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

@@ -13,18 +13,17 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.0.0",
"version": "1.2.1",
"engines": {
"node": ">=14.0"
},
"main": "./index.mjs",
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.2.0",
"@vates/read-chunk": "^1.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.3.4"
"xen-api": "^1.3.3"
},
"devDependencies": {
"tap": "^16.3.0",
@@ -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

@@ -1,8 +1,8 @@
{
"name": "@vates/node-vsphere-soap",
"version": "2.0.0",
"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

@@ -19,7 +19,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "1.2.0",
"version": "1.1.1",
"engines": {
"node": ">=8.10"
},
@@ -33,8 +33,5 @@
},
"devDependencies": {
"test": "^3.2.1"
},
"dependencies": {
"isutf8": "^4.0.0"
}
}

View File

@@ -7,9 +7,9 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.40.0",
"@xen-orchestra/backups": "^0.39.0",
"@xen-orchestra/fs": "^4.0.1",
"filenamify": "^6.0.0",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0"
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.10",
"version": "1.0.9",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -5,7 +5,7 @@ import { createLogger } from '@xen-orchestra/log'
import { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } from 'vhd-lib'
import { decorateMethodsWith } from '@vates/decorate-with'
import { deduped } from '@vates/disposable/deduped.js'
import { dirname, join, resolve } from 'node:path'
import { dirname, join, normalize, resolve } from 'node:path'
import { execFile } from 'child_process'
import { mount } from '@vates/fuse-vhd'
import { readdir, lstat } from 'node:fs/promises'
@@ -18,7 +18,6 @@ import fromEvent from 'promise-toolbox/fromEvent'
import groupBy from 'lodash/groupBy.js'
import pDefer from 'promise-toolbox/defer'
import pickBy from 'lodash/pickBy.js'
import tar from 'tar'
import zlib from 'zlib'
import { BACKUP_DIR } from './_getVmBackupDir.mjs'
@@ -42,23 +41,20 @@ const compareTimestamp = (a, b) => a.timestamp - b.timestamp
const noop = Function.prototype
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
const makeRelative = path => resolve('/', path).slice(1)
const resolveSubpath = (root, path) => resolve(root, makeRelative(path))
async function addZipEntries(zip, realBasePath, virtualBasePath, relativePaths) {
for (const relativePath of relativePaths) {
const realPath = join(realBasePath, relativePath)
const virtualPath = join(virtualBasePath, relativePath)
const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
const stats = await lstat(realPath)
const { mode, mtime } = stats
const opts = { mode, mtime }
if (stats.isDirectory()) {
zip.addEmptyDirectory(virtualPath, opts)
await addZipEntries(zip, realPath, virtualPath, await readdir(realPath))
} else if (stats.isFile()) {
zip.addFile(realPath, virtualPath, opts)
}
async function addDirectory(files, realPath, metadataPath) {
const stats = await lstat(realPath)
if (stats.isDirectory()) {
await asyncMap(await readdir(realPath), file =>
addDirectory(files, realPath + '/' + file, metadataPath + '/' + file)
)
} else if (stats.isFile()) {
files.push({
realPath,
metadataPath,
})
}
}
@@ -186,6 +182,17 @@ export class RemoteAdapter {
})
}
async *_usePartitionFiles(diskId, partitionId, paths) {
const path = yield this.getPartition(diskId, partitionId)
const files = []
await asyncMap(paths, file =>
addDirectory(files, resolveSubpath(path, file), normalize('./' + file).replace(/\/+$/, ''))
)
return files
}
// check if we will be allowed to merge a a vhd created in this adapter
// with the vhd at path `path`
async isMergeableParent(packedParentUid, path) {
@@ -202,24 +209,15 @@ export class RemoteAdapter {
})
}
fetchPartitionFiles(diskId, partitionId, paths, format) {
fetchPartitionFiles(diskId, partitionId, paths) {
const { promise, reject, resolve } = pDefer()
Disposable.use(
async function* () {
const path = yield this.getPartition(diskId, partitionId)
let outputStream
if (format === 'tgz') {
outputStream = tar.c({ cwd: path, gzip: true }, paths.map(makeRelative))
} else if (format === 'zip') {
const zip = new ZipFile()
await addZipEntries(zip, path, '', paths.map(makeRelative))
zip.end()
;({ outputStream } = zip)
} else {
throw new Error('unsupported format ' + format)
}
const files = yield this._usePartitionFiles(diskId, partitionId, paths)
const zip = new ZipFile()
files.forEach(({ realPath, metadataPath }) => zip.addFile(realPath, metadataPath))
zip.end()
const { outputStream } = zip
resolve(outputStream)
await fromEvent(outputStream, 'end')
}.bind(this)
@@ -683,17 +681,11 @@ export class RemoteAdapter {
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
const container = watchStreamSize(input)
const handler = this._handler
await handler.outputStream(path, input, {
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
async validator(tmpPath) {
async validator() {
await input.task
// size on file system can be bigger when encrypted ( IV + alignment padding)
const size = await handler.getSize(tmpPath, { exact: false })
if (Math.abs(size - container.size) > handler.getSizeApproximationMargin()) {
return false
}
return validator.apply(this, arguments)
},
})
@@ -832,6 +824,8 @@ decorateMethodsWith(RemoteAdapter, {
debounceResourceFactory,
]),
_usePartitionFiles: Disposable.factory,
getDisk: compose([Disposable.factory, [deduped, diskId => [diskId]], debounceResourceFactory]),
getPartition: Disposable.factory,

View File

@@ -21,12 +21,7 @@ export class RestoreMetadataBackup {
})
} else {
const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
const dataFileName = resolve(backupId, metadata.data ?? 'data.json')
const data = await handler.readFile(dataFileName)
// if data is JSON, sent it as a plain string, otherwise, consider the data as binary and encode it
const isJson = dataFileName.endsWith('.json')
return isJson ? data.toString() : { encoding: 'base64', data: data.toString('base64') }
return String(await handler.readFile(resolve(backupId, metadata.data ?? 'data.json')))
}
}
}

View File

@@ -16,8 +16,6 @@ export const TAG_BASE_DELTA = 'xo:base_delta'
export const TAG_COPY_SRC = 'xo:copy_of'
const TAG_BACKUP_SR = 'xo:backup:sr'
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
const resolveUuid = async (xapi, cache, uuid, type) => {
if (uuid == null) {
@@ -159,10 +157,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
if (detectBase) {
const remoteBaseVmUuid = vmRecord.other_config[TAG_BASE_DELTA]
if (remoteBaseVmUuid) {
baseVm = find(
xapi.objects.all,
obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid && obj[TAG_BACKUP_SR] === sr.$id
)
baseVm = find(xapi.objects.all, obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid)
if (!baseVm) {
throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)

View File

@@ -22,13 +22,7 @@ export class XoMetadataBackup {
const dir = `${scheduleDir}/${formatFilenameDate(timestamp)}`
const data = job.xoMetadata
let dataBaseName = './data'
// JSON data is sent as plain string, binary data is sent as an object with `data` and `encoding properties
const isJson = typeof data === 'string'
if (isJson) {
dataBaseName += '.json'
}
const dataBaseName = './data.json'
const metadata = JSON.stringify(
{
@@ -60,7 +54,7 @@ export class XoMetadataBackup {
async () => {
const handler = adapter.handler
const dirMode = this._config.dirMode
await handler.outputFile(dataFileName, isJson ? data : Buffer.from(data.data, data.encoding), { dirMode })
await handler.outputFile(dataFileName, data, { dirMode })
await handler.outputFile(metaDataFileName, metadata, {
dirMode,
})

View File

@@ -18,7 +18,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
const vdiRefs = await xapi.VM_getDisks(baseVm.$ref)
for (const vdiRef of vdiRefs) {
const vdi = xapi.getObject(vdiRef)
if (vdi.$SR.uuid !== this._healthCheckSr.uuid) {
if (vdi.$SR.uuid !== this._heathCheckSr.uuid) {
return false
}
}

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.40.0",
"version": "0.39.0",
"engines": {
"node": ">=14.18"
},
@@ -23,15 +23,15 @@
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^2.0.0",
"@vates/nbd-client": "^2.0.0",
"@vates/fuse-vhd": "^1.0.0",
"@vates/nbd-client": "^1.2.1",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^6.0.0",
"d3-time-format": "^4.1.0",
"compare-versions": "^5.0.1",
"d3-time-format": "^3.0.0",
"decorator-synchronized": "^0.6.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",
@@ -40,10 +40,9 @@
"parse-pairs": "^2.0.0",
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"xen-api": "^1.3.4",
"xen-api": "^1.3.3",
"yazl": "^2.5.1"
},
"devDependencies": {
@@ -54,7 +53,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^3.0.0"
"@xen-orchestra/xapi": "^2.2.1"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^1.3.4"
"xen-api": "^1.3.3"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -29,7 +29,7 @@
"@vates/async-each": "^1.0.0",
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/read-chunk": "^1.2.0",
"@vates/read-chunk": "^1.1.1",
"@xen-orchestra/log": "^0.6.0",
"bind-property-descriptor": "^2.0.0",
"decorator-synchronized": "^0.6.0",

View File

@@ -227,19 +227,11 @@ export default class RemoteHandlerAbstract {
// when using encryption, the file size is aligned with the encryption block size ( 16 bytes )
// that means that the size will be 1 to 16 bytes more than the content size + the initialized vector length (16 bytes)
async getSize(file, { exact = true } = {}) {
exact && assert.strictEqual(this.isEncrypted, false, `Can't compute size of an encrypted file ${file}`)
async getSize(file) {
assert.strictEqual(this.isEncrypted, false, `Can't compute size of an encrypted file ${file}`)
const size = await timeout.call(this._getSize(typeof file === 'string' ? normalizePath(file) : file), this._timeout)
return size
}
getSizeApproximationMargin() {
if (this.isEncrypted) {
// on block for initialization vector + at most 1 bloc - 1 byte for aligment padding
return this.#encryptor.ivLength * 2 - 1
}
return 0
return size - this.#encryptor.ivLength
}
async __list(dir, { filter, ignoreMissing = false, prependDir = false } = {}) {

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

@@ -1,4 +1,2 @@
// Keeping this file to prevent applying the global monorepo config for now
module.exports = {
trailingComma: "es5",
};
module.exports = {};

View File

@@ -2,11 +2,8 @@
## **next**
## **0.1.2** (2023-07-28)
- Ability to export selected VMs as CSV file (PR [#6915](https://github.com/vatesfr/xen-orchestra/pull/6915))
- [Pool/VMs] Ability to export selected VMs as JSON file (PR [#6911](https://github.com/vatesfr/xen-orchestra/pull/6911))
- Add Tasks to Pool Dashboard (PR [#6713](https://github.com/vatesfr/xen-orchestra/pull/6713))
## **0.1.1** (2023-07-03)

View File

@@ -1,144 +0,0 @@
<!-- TOC -->
- [XenApiCollection](#xenapicollection)
- [Get the collection](#get-the-collection)
- [Defer the subscription](#defer-the-subscription)
- [Create a dedicated collection](#create-a-dedicated-collection)
- [Alter the collection](#alter-the-collection)
_ [Example 1: Adding props to records](#example-1-adding-props-to-records)
_ [Example 2: Adding props to the collection](#example-2-adding-props-to-the-collection) \* [Example 3, filtering and sorting the collection](#example-3-filtering-and-sorting-the-collection)
<!-- TOC -->
# XenApiCollection
## Get the collection
To retrieve a collection, invoke `useXenApiCollection("VM")`.
By doing this, the current component will be automatically subscribed to the collection and will be updated when the
collection changes.
When the component is unmounted, the subscription will be automatically stopped.
## Defer the subscription
If you don't want to fetch the data of the collection when the component is mounted, you can pass `{ immediate: false }`
as options: `const { start, isStarted } = useXenApiCollection("VM", { immediate: false })`.
Then you subscribe to the collection by calling `start()`.
## Create a dedicated collection
It is recommended to create a dedicated collection composable for each type of record you want to use.
They are stored in `src/composables/xen-api-collection/*-collection.composable.ts`.
```typescript
// src/composables/xen-api-collection/console-collection.composable.ts
export const useConsoleCollection = () => useXenApiCollection("console");
```
If you want to allow the user to defer the subscription, you can propagate the options to `useXenApiCollection`.
```typescript
// console-collection.composable.ts
export const useConsoleCollection = <
Immediate extends boolean = true,
>(options?: {
immediate?: Immediate;
}) => useXenApiCollection("console", options);
```
```typescript
// MyComponent.vue
const collection = useConsoleCollection({ immediate: false });
setTimeout(() => collection.start(), 10000);
```
## Alter the collection
You can alter the collection by overriding parts of it.
### Example 1: Adding props to records
```typescript
// xen-api.ts
export interface XenApiConsole extends XenApiRecord<"console"> {
// ... existing props
someProp: string;
someOtherProp: number;
}
```
```typescript
// console-collection.composable.ts
export const useConsoleCollection = () => {
const collection = useXenApiCollection("console");
const records = computed(() => {
return collection.records.value.map((console) => ({
...console,
someProp: "Some value",
someOtherProp: 42,
}));
});
return {
...collection,
records,
};
};
```
```typescript
const consoleCollection = useConsoleCollection();
consoleCollection.getByUuid("...").someProp; // "Some value"
```
### Example 2: Adding props to the collection
```typescript
// vm-collection.composable.ts
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
return {
...collection,
runningVms: computed(() =>
collection.records.value.filter(
(vm) => vm.power_state === POWER_STATE.RUNNING
)
),
};
};
```
### Example 3, filtering and sorting the collection
```typescript
// vm-collection.composable.ts
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
return {
...collection,
records: computed(() =>
collection.records.value
.filter(
(vm) =>
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain
)
.sort((vm1, vm2) => vm1.name_label.localeCompare(vm2.name_label))
),
};
};
```

View File

@@ -0,0 +1,144 @@
# Stores for XenApiRecord collections
All collections of `XenApiRecord` are stored inside the `xapiCollectionStore`.
To retrieve a collection, invoke `useXapiCollectionStore().get(type)`.
## Accessing a collection
In order to use a collection, you'll need to subscribe to it.
```typescript
const consoleStore = useXapiCollectionStore().get("console");
const { records, getByUuid /* ... */ } = consoleStore.subscribe();
```
## Deferred subscription
If you wish to initialize the subscription on demand, you can pass `{ immediate: false }` as options to `subscribe()`.
```typescript
const consoleStore = useXapiCollectionStore().get("console");
const { records, start, isStarted /* ... */ } = consoleStore.subscribe({
immediate: false,
});
// Later, you can then use start() to initialize the subscription.
```
## Create a dedicated store for a collection
To create a dedicated store for a specific `XenApiRecord`, simply return the collection from the XAPI Collection Store:
```typescript
export const useConsoleStore = defineStore("console", () =>
useXapiCollectionStore().get("console")
);
```
## Extending the base Subscription
To extend the base Subscription, you'll need to override the `subscribe` method.
For that, you can use the `createSubscribe<XenApiRecord, Extensions>((options) => { /* ... */})` helper.
### Define the extensions
Subscription extensions are defined as `(object | [object, RequiredOptions])[]`.
When using a tuple (`[object, RequiredOptions]`), the corresponding `object` type will be added to the subscription if
the `RequiredOptions` for that tuple are present in the options passed to `subscribe`.
```typescript
// Always present extension
type DefaultExtension = {
propA: string;
propB: ComputedRef<number>;
};
// Conditional extension 1
type FirstConditionalExtension = [
{ propC: ComputedRef<string> }, // <- This signature will be added
{ optC: string } // <- if this condition is met
];
// Conditional extension 2
type SecondConditionalExtension = [
{ propD: () => void }, // <- This signature will be added
{ optD: number } // <- if this condition is met
];
// Create the extensions array
type Extensions = [
DefaultExtension,
FirstConditionalExtension,
SecondConditionalExtension
];
```
### Define the subscription
```typescript
export const useConsoleStore = defineStore("console", () => {
const consoleCollection = useXapiCollectionStore().get("console");
const subscribe = createSubscribe<XenApiConsole, Extensions>((options) => {
const originalSubscription = consoleCollection.subscribe(options);
const extendedSubscription = {
propA: "Some string",
propB: computed(() => 42),
};
const propCSubscription = options?.optC !== undefined && {
propC: computed(() => "Some other string"),
};
const propDSubscription = options?.optD !== undefined && {
propD: () => console.log("Hello"),
};
return {
...originalSubscription,
...extendedSubscription,
...propCSubscription,
...propDSubscription,
};
});
return {
...consoleCollection,
subscribe,
};
});
```
The generated `subscribe` method will then automatically have the following `options` signature:
```typescript
type Options = {
immediate?: false;
optC?: string;
optD?: number;
};
```
### Use the subscription
In each case, all the default properties (`records`, `getByUuid`, etc.) will be present.
```typescript
const store = useConsoleStore();
// No options (propA and propB will be present)
const subscription = store.subscribe();
// optC option (propA, propB and propC will be present)
const subscription = store.subscribe({ optC: "Hello" });
// optD option (propA, propB and propD will be present)
const subscription = store.subscribe({ optD: 12 });
// optC and optD options (propA, propB, propC and propD will be present)
const subscription = store.subscribe({ optC: "Hello", optD: 12 });
```

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XO Lite</title>
<title>Vite App</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.2",
"version": "0.1.1",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",
@@ -20,9 +20,10 @@
"@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.1",
"complex-matcher": "^0.7.0",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.3.3",
@@ -49,7 +50,7 @@
"@rushstack/eslint-patch": "^1.1.0",
"@types/node": "^16.11.41",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint-plugin-vue": "^9.0.0",

View File

@@ -4,10 +4,10 @@
<AppLogin />
</div>
<div v-else>
<AppHeader v-if="uiStore.hasUi" />
<AppHeader />
<div style="display: flex">
<AppNavigation v-if="uiStore.hasUi" />
<main class="main" :class="{ 'no-ui': !uiStore.hasUi }">
<AppNavigation />
<main class="main">
<RouterView />
</main>
</div>
@@ -23,7 +23,7 @@ import AppNavigation from "@/components/AppNavigation.vue";
import AppTooltips from "@/components/AppTooltips.vue";
import UnreachableHostsModal from "@/components/UnreachableHostsModal.vue";
import { useChartTheme } from "@/composables/chart-theme.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
import { useUiStore } from "@/stores/ui.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
@@ -41,10 +41,10 @@ if (link == null) {
}
link.href = favicon;
document.title = "XO Lite";
const xenApiStore = useXenApiStore();
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
useChartTheme();
const uiStore = useUiStore();
@@ -92,9 +92,5 @@ whenever(
flex: 1;
height: calc(100vh - 8rem);
background-color: var(--background-color-secondary);
&.no-ui {
height: 100vh;
}
}
</style>

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 63 KiB

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

@@ -24,7 +24,6 @@
</template>
<script lang="ts" setup>
import { usePageTitleStore } from "@/stores/page-title.store";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
@@ -34,7 +33,6 @@ import UiButton from "@/components/ui/UiButton.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
const { t } = useI18n();
usePageTitleStore().setTitle(t("login"));
const xenApiStore = useXenApiStore();
const { isConnecting } = storeToRefs(xenApiStore);
const login = ref("root");
@@ -64,7 +62,7 @@ async function handleSubmit() {
isInvalidPassword.value = true;
error.value = t("password-invalid");
} else {
error.value = t("error-occurred");
error.value = t("error-occured");
console.error(err);
}
}

View File

@@ -7,12 +7,12 @@
</template>
<script
generic="T extends XenApiRecord<RawObjectType>, I extends T['uuid']"
generic="T extends XenApiRecord<string>, I extends T['uuid']"
lang="ts"
setup
>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
import type { XenApiRecord } from "@/libs/xen-api";
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
import { computed } from "vue";
import { useRouter } from "vue-router";

View File

@@ -3,11 +3,11 @@
</template>
<script lang="ts" setup>
import { useXenApiStore } from "@/stores/xen-api.store";
import VncClient from "@novnc/novnc/core/rfb";
import { promiseTimeout } from "@vueuse/shared";
import { fibonacci } from "iterable-backoff";
import { computed, onBeforeUnmount, ref, watchEffect } from "vue";
import VncClient from "@novnc/novnc/core/rfb";
import { useXenApiStore } from "@/stores/xen-api.store";
import { promiseTimeout } from "@vueuse/shared";
const N_TOTAL_TRIES = 8;
const FIBONACCI_MS_ARRAY: number[] = Array.from(

View File

@@ -27,14 +27,14 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { faServer } from "@fortawesome/free-solid-svg-icons";
import UiModal from "@/components/ui/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { computed, ref, watch } from "vue";
import { difference } from "lodash-es";
import { useHostStore } from "@/stores/host.store";
const { records: hosts } = useHostCollection();
const { records: hosts } = useHostStore().subscribe();
const unreachableHostsUrls = ref<Set<string>>(new Set());
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);

View File

@@ -33,7 +33,7 @@
</AppMenu>
</UiTabBar>
<div :class="{ 'full-width': fullWidthComponent }" class="tabs">
<div class="tabs">
<UiCard v-if="selectedTab === TAB.NONE" class="tab-content">
<i>No configuration defined</i>
</UiCard>
@@ -102,11 +102,11 @@ import StorySettingParams from "@/components/component-story/StorySettingParams.
import StorySlotParams from "@/components/component-story/StorySlotParams.vue";
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTab from "@/components/ui/UiTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import {
@@ -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[]) =>
@@ -140,7 +148,6 @@ const props = defineProps<{
settings?: Record<string, any>;
}
>;
fullWidthComponent?: boolean;
}>();
enum TAB {
@@ -179,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 }[] }[]
@@ -237,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) => {
@@ -330,10 +359,6 @@ const applyPreset = (preset: {
padding: 1rem;
gap: 1rem;
&.full-width {
flex-direction: column;
}
.tab-content {
flex: 1;
height: auto;

View File

@@ -2,80 +2,32 @@
<RouterLink :to="{ name: 'story' }">
<UiTitle type="h4">Stories</UiTitle>
</RouterLink>
<StoryMenuTree
:tree="tree"
@toggle-directory="toggleDirectory"
:opened-directories="openedDirectories"
/>
<ul class="links">
<li v-for="route in routes" :key="route.name">
<RouterLink class="link" :to="route">
{{ route.meta.storyTitle }}
</RouterLink>
</li>
</ul>
</template>
<script lang="ts" setup>
import StoryMenuTree from "@/components/component-story/StoryMenuTree.vue";
import { useRouter } from "vue-router";
import UiTitle from "@/components/ui/UiTitle.vue";
import { type RouteRecordNormalized, useRoute, useRouter } from "vue-router";
import { ref } from "vue";
const { getRoutes } = useRouter();
const routes = getRoutes().filter((route) => route.meta.isStory);
export type StoryTree = Map<
string,
{ path: string; directory: string; children: StoryTree }
>;
function createTree(routes: RouteRecordNormalized[]) {
const tree: StoryTree = new Map();
for (const route of routes) {
const parts = route.path.slice(7).split("/");
let currentNode = tree;
let currentPath = "";
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
if (!currentNode.has(part)) {
currentNode.set(part, {
children: new Map(),
path: route.path,
directory: currentPath,
});
}
currentNode = currentNode.get(part)!.children;
}
}
return tree;
}
const tree = createTree(routes);
const currentRoute = useRoute();
const getDefaultOpenedDirectories = (): Set<string> => {
if (!currentRoute.meta.isStory) {
return new Set<string>();
}
const openedDirectories = new Set<string>();
const parts = currentRoute.path.split("/");
let currentPath = "";
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
openedDirectories.add(currentPath);
}
return openedDirectories;
};
const openedDirectories = ref(getDefaultOpenedDirectories());
const toggleDirectory = (directory: string) => {
if (openedDirectories.value.has(directory)) {
openedDirectories.value.delete(directory);
} else {
openedDirectories.value.add(directory);
}
};
</script>
<style lang="postcss" scoped>
.links {
padding: 1rem;
}
.link {
display: inline-block;
padding: 0.5rem 1rem;
text-decoration: none;
font-size: 1.6rem;
}
</style>

View File

@@ -1,83 +0,0 @@
<template>
<ul class="story-menu-tree">
<li v-for="[key, node] in tree" :key="key">
<span
v-if="node.children.size > 0"
class="directory"
@click="emit('toggle-directory', node.directory)"
>
<UiIcon
:icon="isOpen(node.directory) ? faFolderOpen : faFolderClosed"
/>
{{ formatName(key) }}
</span>
<RouterLink v-else :to="node.path" class="link">
<UiIcon :icon="faFile" />
{{ formatName(key) }}
</RouterLink>
<StoryMenuTree
v-if="isOpen(node.directory)"
:tree="node.children"
@toggle-directory="emit('toggle-directory', $event)"
:opened-directories="openedDirectories"
/>
</li>
</ul>
</template>
<script lang="ts" setup>
import type { StoryTree } from "@/components/component-story/StoryMenu.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import {
faFile,
faFolderClosed,
faFolderOpen,
} from "@fortawesome/free-regular-svg-icons";
const props = defineProps<{
tree: StoryTree;
openedDirectories: Set<string>;
}>();
const emit = defineEmits<{
(event: "toggle-directory", directory: string): void;
}>();
const isOpen = (directory: string) => props.openedDirectories.has(directory);
const formatName = (name: string) => {
const parts = name.split("-");
return parts.map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
};
</script>
<style lang="postcss" scoped>
.story-menu-tree {
padding-left: 1rem;
.story-menu-tree {
padding-left: 2.2rem;
}
}
.directory {
font-weight: 500;
}
.link {
padding: 0.5rem 0;
}
.directory {
padding: 0.5rem 0;
}
.link,
.directory {
cursor: pointer;
text-decoration: none;
font-size: 1.6rem;
display: inline-block;
}
</style>

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

@@ -28,10 +28,10 @@
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiHost } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useUiStore } from "@/stores/ui.store";
import {
faAngleDown,
@@ -46,10 +46,11 @@ const props = defineProps<{
hostOpaqueRef: XenApiHost["$ref"];
}>();
const { getByOpaqueRef } = useHostCollection();
const { getByOpaqueRef } = useHostStore().subscribe();
const host = computed(() => getByOpaqueRef(props.hostOpaqueRef));
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
const isPoolMaster = computed(() => pool.value?.master === props.hostOpaqueRef);
const uiStore = useUiStore();

View File

@@ -16,9 +16,9 @@
<script lang="ts" setup>
import InfraHostItem from "@/components/infra/InfraHostItem.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useHostStore } from "@/stores/host.store";
const { records: hosts, isReady, hasError } = useHostCollection();
const { records: hosts, isReady, hasError } = useHostStore().subscribe();
</script>
<style lang="postcss" scoped>

View File

@@ -28,10 +28,10 @@ import InfraHostList from "@/components/infra/InfraHostList.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
const { isReady, hasError, pool } = usePoolCollection();
const { isReady, hasError, pool } = usePoolStore().subscribe();
</script>
<style lang="postcss" scoped>

View File

@@ -19,8 +19,8 @@
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import type { XenApiVm } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { useIntersectionObserver } from "@vueuse/core";
import { computed, ref } from "vue";
@@ -29,7 +29,7 @@ const props = defineProps<{
vmOpaqueRef: XenApiVm["$ref"];
}>();
const { getByOpaqueRef } = useVmCollection();
const { getByOpaqueRef } = useVmStore().subscribe();
const vm = computed(() => getByOpaqueRef(props.vmOpaqueRef));
const rootElement = ref();
const isVisible = ref(false);

View File

@@ -11,8 +11,8 @@
<script lang="ts" setup>
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmItem from "@/components/infra/InfraVmItem.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import type { XenApiHost } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -20,7 +20,7 @@ const props = defineProps<{
hostOpaqueRef?: XenApiHost["$ref"];
}>();
const { isReady, recordsByHostRef, hasError } = useVmCollection();
const { isReady, recordsByHostRef, hasError } = useVmStore().subscribe();
const vms = computed(() =>
recordsByHostRef.value.get(

View File

@@ -5,12 +5,12 @@
</template>
<script lang="ts" setup>
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { computed } from "vue";
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
import TitleBar from "@/components/TitleBar.vue";
import { usePoolStore } from "@/stores/pool.store";
const { pool } = usePoolCollection();
const { pool } = usePoolStore().subscribe();
const name = computed(() => pool.value?.name_label ?? "...");
</script>

View File

@@ -33,7 +33,7 @@
<script lang="ts" setup>
import RouterTab from "@/components/RouterTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { usePoolStore } from "@/stores/pool.store";
const { pool, isReady } = usePoolCollection();
const { pool, isReady } = usePoolStore().subscribe();
</script>

View File

@@ -37,11 +37,12 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { useVmMetricsCollection } from "@/composables/xen-api-collection/vm-metrics-collection.composable";
import { percent } from "@/libs/utils";
import { POWER_STATE } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import { useVmMetricsStore } from "@/stores/vm-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
@@ -51,16 +52,18 @@ const {
hasError: hostStoreHasError,
isReady: isHostStoreReady,
runningHosts,
} = useHostCollection();
} = useHostStore().subscribe({
hostMetricsSubscription: useHostMetricsStore().subscribe(),
});
const {
hasError: vmStoreHasError,
isReady: isVmStoreReady,
records: vms,
} = useVmCollection();
} = useVmStore().subscribe();
const { getByOpaqueRef: getVmMetrics, isReady: isVmMetricsStoreReady } =
useVmMetricsCollection();
useVmMetricsStore().subscribe();
const nPCpu = computed(() =>
runningHosts.value.reduce(

View File

@@ -11,20 +11,20 @@
</UiCard>
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject, type ComputedRef } from "vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import UiSpinner from "@/components/ui/UiSpinner.vue";
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -12,21 +12,21 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsRamUsage from "@/components/pool/dashboard/ramUsage/HostsRamUsage.vue";
import VmsRamUsage from "@/components/pool/dashboard/ramUsage/VmsRamUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject } from "vue";
import type { ComputedRef } from "vue";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import type { Stat } from "@/composables/fetch-stats.composable";
import UiSpinner from "@/components/ui/UiSpinner.vue";
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -26,21 +26,22 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSeparator from "@/components/ui/UiSeparator.vue";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { computed } from "vue";
const {
isReady: isVmReady,
records: vms,
hasError: hasVmError,
} = useVmCollection();
runningVms,
} = useVmStore().subscribe();
const {
isReady: isHostMetricsReady,
records: hostMetrics,
hasError: hasHostMetricsError,
} = useHostMetricsCollection();
} = useHostMetricsStore().subscribe();
const hasError = computed(() => hasVmError.value || hasHostMetricsError.value);
@@ -54,7 +55,5 @@ const activeHostsCount = computed(
const totalVmsCount = computed(() => vms.value.length);
const activeVmsCount = computed(
() => vms.value.filter((vm) => vm.power_state === "Running").length
);
const activeVmsCount = computed(() => runningVms.value.length);
</script>

View File

@@ -23,11 +23,11 @@ import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useSrCollection } from "@/composables/xen-api-collection/sr-collection.composable";
import { useSrStore } from "@/stores/storage.store";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed } from "vue";
const { records: srs, isReady, hasError } = useSrCollection();
const { records: srs, isReady, hasError } = useSrStore().subscribe();
const data = computed<{
result: { id: string; label: string; value: number }[];

View File

@@ -1,17 +0,0 @@
<template>
<UiCard>
<UiCardTitle :count="pendingTasks.length">{{ $t("tasks") }}</UiCardTitle>
<TasksTable :pending-tasks="pendingTasks" />
</UiCard>
</template>
<script lang="ts" setup>
import TasksTable from "@/components/tasks/TasksTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useTaskCollection } from "@/composables/xen-api-collection/task-collection.composable";
const { pendingTasks } = useTaskCollection();
</script>
<style lang="postcss" scoped></style>

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import { useHostStore } from "@/stores/host.store";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useHostCollection();
const { hasError } = useHostStore().subscribe();
const stats = inject(
IK_HOST_STATS,

View File

@@ -12,9 +12,9 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import type { HostStats } from "@/libs/xapi-stats";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
@@ -29,7 +29,7 @@ const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { records: hosts } = useHostCollection();
const { records: hosts } = useHostStore().subscribe();
const customMaxValue = computed(
() => 100 * sumBy(hosts.value, (host) => +host.cpu_info.cpu_count)

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import { useVmStore } from "@/stores/vm.store";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useVmCollection();
const { hasError } = useVmStore().subscribe();
const stats = inject(
IK_VM_STATS,

View File

@@ -10,15 +10,15 @@
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import NoDataError from "@/components/NoDataError.vue";
import { useHostStore } from "@/stores/host.store";
const { hasError } = useHostCollection();
const { hasError } = useHostStore().subscribe();
const stats = inject(
IK_HOST_STATS,

View File

@@ -17,10 +17,10 @@
<script lang="ts" setup>
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import { formatSize } from "@/libs/utils";
import { formatSize, getHostMemory } from "@/libs/utils";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
@@ -31,22 +31,27 @@ const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { runningHosts } = useHostCollection();
const { getHostMemory } = useHostMetricsCollection();
const hostMetricsSubscription = useHostMetricsStore().subscribe();
const hostStore = useHostStore();
const { runningHosts } = hostStore.subscribe({ hostMetricsSubscription });
const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const customMaxValue = computed(() =>
sumBy(runningHosts.value, (host) => getHostMemory(host)?.size ?? 0)
sumBy(
runningHosts.value,
(host) => getHostMemory(host, hostMetricsSubscription)?.size ?? 0
)
);
const currentData = computed(() => {
let size = 0,
usage = 0;
runningHosts.value.forEach((host) => {
const hostMemory = getHostMemory(host);
const hostMemory = getHostMemory(host, hostMetricsSubscription);
size += hostMemory?.size ?? 0;
usage += hostMemory?.usage ?? 0;
});

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { useVmStore } from "@/stores/vm.store";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useVmCollection();
const { hasError } = useVmStore().subscribe();
const stats = inject(
IK_VM_STATS,

View File

@@ -34,9 +34,9 @@
<script lang="ts" setup>
import RelativeTime from "@/components/RelativeTime.vue";
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { parseDateTime } from "@/libs/utils";
import type { XenApiTask } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { computed } from "vue";
const props = defineProps<{
@@ -44,7 +44,7 @@ const props = defineProps<{
task: XenApiTask;
}>();
const { getByOpaqueRef: getHost } = useHostCollection();
const { getByOpaqueRef: getHost } = useHostStore().subscribe();
const createdAt = computed(() => parseDateTime(props.task.created));

View File

@@ -1,5 +1,5 @@
<template>
<UiTable :color="hasError ? 'error' : undefined" class="tasks-table">
<UiTable class="tasks-table" :color="hasError ? 'error' : undefined">
<thead>
<tr>
<th>{{ $t("name") }}</th>
@@ -20,9 +20,6 @@
<UiSpinner class="loader" />
</td>
</tr>
<tr v-else-if="!hasTasks">
<td class="no-tasks" colspan="5">{{ $t("no-tasks") }}</td>
</tr>
<template v-else>
<TaskRow
v-for="task in pendingTasks"
@@ -38,35 +35,20 @@
<script lang="ts" setup>
import TaskRow from "@/components/tasks/TaskRow.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTable from "@/components/ui/UiTable.vue";
import { useTaskCollection } from "@/composables/xen-api-collection/task-collection.composable";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useTaskStore } from "@/stores/task.store";
import type { XenApiTask } from "@/libs/xen-api";
import { computed } from "vue";
const props = defineProps<{
defineProps<{
pendingTasks: XenApiTask[];
finishedTasks?: XenApiTask[];
finishedTasks: XenApiTask[];
}>();
const { hasError, isFetching } = useTaskCollection();
const hasTasks = computed(
() => props.pendingTasks.length > 0 || (props.finishedTasks?.length ?? 0) > 0
);
const { hasError, isFetching } = useTaskStore().subscribe();
</script>
<style lang="postcss" scoped>
.tasks-table {
width: 100%;
}
.no-tasks {
text-align: center;
color: var(--color-blue-scale-300);
font-style: italic;
}
td[colspan="5"] {
text-align: center;
}

View File

@@ -6,7 +6,6 @@
class="left"
>
<slot>{{ left }}</slot>
<UiCounter class="count" v-if="count > 0" :value="count" color="info" />
</component>
<component
:is="subtitle ? 'h6' : 'h5'"
@@ -19,17 +18,11 @@
</template>
<script lang="ts" setup>
import UiCounter from "@/components/ui/UiCounter.vue";
withDefaults(
defineProps<{
subtitle?: boolean;
left?: string;
right?: string;
count?: number;
}>(),
{ count: 0 }
);
defineProps<{
subtitle?: boolean;
left?: string;
right?: string;
}>();
</script>
<style lang="postcss" scoped>
@@ -62,9 +55,6 @@ withDefaults(
font-size: var(--section-title-left-size);
font-weight: var(--section-title-left-weight);
color: var(--section-title-left-color);
display: flex;
align-items: center;
gap: 2rem;
}
.right {
@@ -72,8 +62,4 @@ withDefaults(
font-weight: var(--section-title-right-weight);
color: var(--section-title-right-color);
}
.count {
font-size: 1.6rem;
}
</style>

View File

@@ -1,50 +0,0 @@
<template>
<li class="ui-resource">
<UiIcon :icon="icon" class="icon" />
<div class="separator" />
<div class="label">{{ label }}</div>
<div class="count">{{ count }}</div>
</li>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{
icon: IconDefinition;
label: string;
count: string | number;
}>();
</script>
<style lang="postcss" scoped>
.ui-resource {
display: flex;
align-items: center;
}
.icon {
color: var(--color-extra-blue-base);
font-size: 3.2rem;
}
.separator {
height: 4.5rem;
width: 0;
border-left: 0.1rem solid var(--color-extra-blue-base);
background-color: var(--color-extra-blue-base);
margin: 0 1.5rem;
}
.label {
font-size: 1.6rem;
font-weight: 700;
}
.count {
font-size: 1.4rem;
font-weight: 400;
margin-left: 2rem;
}
</style>

View File

@@ -1,13 +0,0 @@
<template>
<ul class="ui-resources">
<slot />
</ul>
</template>
<style lang="postcss" scoped>
.ui-resources {
display: flex;
gap: 1rem 5.4rem;
flex-wrap: wrap;
}
</style>

View File

@@ -12,9 +12,10 @@
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import { isOperationsPending } from "@/libs/utils";
import { POWER_STATE, VM_OPERATION, type XenApiVm } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCopy } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -23,7 +24,7 @@ const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef, isOperationPending } = useVmCollection();
const { getByOpaqueRef } = useVmStore().subscribe();
const selectedVms = computed(() =>
props.selectedRefs
@@ -38,7 +39,7 @@ const areAllSelectedVmsHalted = computed(() =>
);
const areSomeSelectedVmsCloning = computed(() =>
selectedVms.value.some((vm) => isOperationPending(vm, VM_OPERATION.CLONE))
selectedVms.value.some((vm) => isOperationsPending(vm, VM_OPERATION.CLONE))
);
const handleCopy = async () => {

View File

@@ -35,11 +35,11 @@
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { POWER_STATE } from "@/libs/xen-api";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faSatellite, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -51,7 +51,7 @@ const props = defineProps<{
}>();
const xenApi = useXenApiStore().getXapi();
const { getByOpaqueRef: getVm } = useVmCollection();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const {
open: openDeleteModal,
close: closeDeleteModal,

View File

@@ -27,7 +27,6 @@
</template>
<script lang="ts" setup>
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { computed } from "vue";
import { exportVmsAsCsvFile, exportVmsAsJsonFile } from "@/libs/vm";
import MenuItem from "@/components/menu/MenuItem.vue";
@@ -37,6 +36,7 @@ import {
faFileCsv,
faFileExport,
} from "@fortawesome/free-solid-svg-icons";
import { useVmStore } from "@/stores/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api";
@@ -44,7 +44,7 @@ const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef: getVm } = useVmCollection();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const vms = computed(() =>
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
);

View File

@@ -95,12 +95,13 @@
import MenuItem from "@/components/menu/MenuItem.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import { usePoolCollection } from "@/composables/xen-api-collection/pool-collection.composable";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import { isHostRunning, isOperationsPending } from "@/libs/utils";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import {
faCirclePlay,
@@ -120,12 +121,12 @@ const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef: getVm, isOperationPending } = useVmCollection();
const { records: hosts } = useHostCollection();
const { pool } = usePoolCollection();
const { isHostRunning } = useHostMetricsCollection();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const { records: hosts } = useHostStore().subscribe();
const { pool } = usePoolStore().subscribe();
const hostMetricsSubscription = useHostMetricsStore().subscribe();
const vms = computed(() =>
const vms = computed<XenApiVm[]>(() =>
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
);
@@ -149,7 +150,7 @@ const areVmsPaused = computed(() =>
);
const areOperationsPending = (operation: VM_OPERATION | VM_OPERATION[]) =>
vms.value.some((vm) => isOperationPending(vm, operation));
vms.value.some((vm) => isOperationsPending(vm, operation));
const areVmsBusyToStart = computed(() =>
areOperationsPending(VM_OPERATION.START)
@@ -179,7 +180,9 @@ const areVmsBusyToForceShutdown = computed(() =>
areOperationsPending(VM_OPERATION.HARD_SHUTDOWN)
);
const getHostState = (host: XenApiHost) =>
isHostRunning(host) ? POWER_STATE.RUNNING : POWER_STATE.HALTED;
isHostRunning(host, hostMetricsSubscription)
? POWER_STATE.RUNNING
: POWER_STATE.HALTED;
</script>
<style lang="postcss" scoped>

View File

@@ -20,8 +20,8 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import TitleBar from "@/components/TitleBar.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useVmStore } from "@/stores/vm.store";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import { useVmCollection } from "@/composables/xen-api-collection/vm-collection.composable";
import type { XenApiVm } from "@/libs/xen-api";
import {
faAngleDown,
@@ -31,7 +31,7 @@ import {
import { computed } from "vue";
import { useRouter } from "vue-router";
const { getByUuid: getVmByUuid } = useVmCollection();
const { getByUuid: getVmByUuid } = useVmStore().subscribe();
const { currentRoute } = useRouter();
const vm = computed(() =>

View File

@@ -17,9 +17,9 @@ export type Stat<T> = {
pausable: Pausable;
};
export type GetStats<
type GetStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats = T extends XenApiHost ? HostStats : VmStats,
S extends HostStats | VmStats
> = (
uuid: T["uuid"],
granularity: GRANULARITY,
@@ -29,7 +29,7 @@ export type GetStats<
export type FetchedStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats = T extends XenApiHost ? HostStats : VmStats,
S extends HostStats | VmStats
> = {
register: (object: T) => void;
unregister: (object: T) => void;
@@ -40,7 +40,7 @@ export type FetchedStats<
export default function useFetchStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats = T extends XenApiHost ? HostStats : VmStats,
S extends HostStats | VmStats
>(getStats: GetStats<T, S>, granularity: GRANULARITY): FetchedStats<T, S> {
const stats = ref<Map<string, Stat<S>>>(new Map());
const timestamp = ref<number[]>([0, 0]);
@@ -108,7 +108,7 @@ export default function useFetchStats<
return {
register,
unregister,
stats: computed(() => Array.from(stats.value.values()) as Stat<S>[]),
stats: computed<Stat<S>[]>(() => Array.from(stats.value.values())),
timestampStart: computed(() => timestamp.value[0]),
timestampEnd: computed(() => timestamp.value[1]),
};

View File

@@ -1,63 +0,0 @@
import type { RawObjectType } from "@/libs/xen-api";
import { getXenApiCollection } from "@/libs/xen-api-collection";
import type {
RawTypeToRecord,
XenApiBaseCollection,
XenApiCollectionManager,
} from "@/types/xen-api-collection";
import { tryOnUnmounted } from "@vueuse/core";
import { computed } from "vue";
export const useXenApiCollection = <
ObjectType extends RawObjectType,
Record extends RawTypeToRecord<ObjectType>,
Immediate extends boolean,
>(
type: ObjectType,
options?: { immediate?: Immediate }
): XenApiBaseCollection<Record, Immediate> => {
const baseCollection = getXenApiCollection(type);
const isDeferred = options?.immediate === false;
const id = Symbol();
const collection = {
records: baseCollection.records,
isFetching: baseCollection.isFetching,
isReloading: baseCollection.isReloading,
isReady: baseCollection.isReady,
hasError: baseCollection.hasError,
hasUuid: baseCollection.hasUuid.bind(baseCollection),
getByUuid: baseCollection.getByUuid.bind(baseCollection),
getByOpaqueRef: baseCollection.getByOpaqueRef.bind(baseCollection),
};
tryOnUnmounted(() => baseCollection.unsubscribe(id));
if (isDeferred) {
return {
...collection,
start: () => baseCollection.subscribe(id),
isStarted: computed(() => baseCollection.hasSubscriptions.value),
} as XenApiBaseCollection<Record, false>;
}
baseCollection.subscribe(id);
return collection as XenApiBaseCollection<Record, Immediate>;
};
export const useXenApiCollectionManager = <
ObjectType extends RawObjectType,
Record extends RawTypeToRecord<ObjectType>,
>(
type: ObjectType
): XenApiCollectionManager<Record> => {
const collection = getXenApiCollection(type);
return {
hasSubscriptions: collection.hasSubscriptions,
add: collection.add.bind(collection),
remove: collection.remove.bind(collection),
update: collection.update.bind(collection),
};
};

View File

@@ -1,3 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
export const useConsoleCollection = () => useXenApiCollection("console");

View File

@@ -1,45 +0,0 @@
import type { GetStats } from "@/composables/fetch-stats.composable";
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import { useHostMetricsCollection } from "@/composables/xen-api-collection/host-metrics-collection.composable";
import type { XenApiHost } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import { computed } from "vue";
export const useHostCollection = () => {
const collection = useXenApiCollection("host");
const hostMetricsCollection = useHostMetricsCollection();
return {
...collection,
runningHosts: computed(() =>
collection.records.value.filter((host) =>
hostMetricsCollection.isHostRunning(host)
)
),
getStats: ((
hostUuid,
granularity,
ignoreExpired = false,
{ abortSignal }
) => {
const xenApiStore = useXenApiStore();
const host = collection.getByUuid(hostUuid);
if (host === undefined) {
throw new Error(`Host ${hostUuid} could not be found.`);
}
const xapiStats = xenApiStore.isConnected
? xenApiStore.getXapiStats()
: undefined;
return xapiStats?._getAndUpdateStats({
abortSignal,
host,
ignoreExpired,
uuid: host.uuid,
granularity,
});
}) as GetStats<XenApiHost>,
};
};

View File

@@ -1,24 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import type { XenApiHost } from "@/libs/xen-api";
export const useHostMetricsCollection = () => {
const collection = useXenApiCollection("host_metrics");
return {
...collection,
getHostMemory: (host: XenApiHost) => {
const hostMetrics = collection.getByOpaqueRef(host.metrics);
if (hostMetrics !== undefined) {
const total = +hostMetrics.memory_total;
return {
usage: total - +hostMetrics.memory_free,
size: total,
};
}
},
isHostRunning: (host: XenApiHost) => {
return collection.getByOpaqueRef(host.metrics)?.live === true;
},
};
};

View File

@@ -1,14 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import type { XenApiPool } from "@/libs/xen-api";
import { computed } from "vue";
export const usePoolCollection = () => {
const poolCollection = useXenApiCollection("pool");
return {
...poolCollection,
pool: computed<XenApiPool | undefined>(
() => poolCollection.records.value[0]
),
};
};

View File

@@ -1,3 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
export const useSrCollection = () => useXenApiCollection("SR");

View File

@@ -1,41 +0,0 @@
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
import useCollectionFilter from "@/composables/collection-filter.composable";
import useCollectionSorter from "@/composables/collection-sorter.composable";
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiTask } from "@/libs/xen-api";
export const useTaskCollection = () => {
const collection = useXenApiCollection("task");
const { compareFn } = useCollectionSorter<XenApiTask>({
initialSorts: ["-created"],
});
const { predicate } = useCollectionFilter({
initialFilters: [
"!name_label:|(SR.scan host.call_plugin)",
"status:pending",
],
});
const sortedTasks = useSortedCollection(collection.records, compareFn);
return {
...collection,
pendingTasks: useFilteredCollection<XenApiTask>(sortedTasks, predicate),
finishedTasks: useArrayRemovedItemsHistory(
sortedTasks,
(task) => task.uuid,
{
limit: 50,
onRemove: (tasks) =>
tasks.map((task) => ({
...task,
finished: new Date().toISOString(),
})),
}
),
};
};

View File

@@ -1,83 +0,0 @@
import type { GetStats } from "@/composables/fetch-stats.composable";
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
import { useHostCollection } from "@/composables/xen-api-collection/host-collection.composable";
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import {
POWER_STATE,
VM_OPERATION,
type XenApiHost,
type XenApiVm,
} from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import { castArray } from "lodash-es";
import { computed } from "vue";
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
const hostCollection = useHostCollection();
const xenApiStore = useXenApiStore();
const records = computed(() =>
collection.records.value
.filter(
(vm) => !vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain
)
.sort(sortRecordsByNameLabel)
);
return {
...collection,
records,
isOperationPending: (
vm: XenApiVm,
operations: VM_OPERATION[] | VM_OPERATION
) => {
const currentOperations = Object.values(vm.current_operations);
return castArray(operations).some((operation) =>
currentOperations.includes(operation)
);
},
runningVms: computed(() =>
records.value.filter((vm) => vm.power_state === POWER_STATE.RUNNING)
),
recordsByHostRef: computed(() => {
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
records.value.forEach((vm) => {
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
vmsByHostOpaqueRef.set(vm.resident_on, []);
}
vmsByHostOpaqueRef.get(vm.resident_on)?.push(vm);
});
return vmsByHostOpaqueRef;
}),
getStats: ((id, granularity, ignoreExpired = false, { abortSignal }) => {
if (!xenApiStore.isConnected) {
return undefined;
}
const vm = collection.getByUuid(id);
if (vm === undefined) {
throw new Error(`VM ${id} could not be found.`);
}
const host = hostCollection.getByOpaqueRef(vm.resident_on);
if (host === undefined) {
throw new Error(`VM ${id} is halted or host could not be found.`);
}
return xenApiStore.getXapiStats()._getAndUpdateStats<VmStats>({
abortSignal,
host,
ignoreExpired,
uuid: vm.uuid,
granularity,
});
}) as GetStats<XenApiVm>,
};
};

View File

@@ -1,3 +0,0 @@
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
export const useVmMetricsCollection = () => useXenApiCollection("VM_metrics");

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

@@ -1,14 +1,18 @@
import type {
RawXenApiRecord,
XenApiHost,
XenApiHostMetrics,
XenApiRecord,
RawObjectType,
XenApiVm,
VM_OPERATION,
} from "@/libs/xen-api";
import type { Filter } from "@/types/filter";
import type { Subscription } from "@/types/xapi-collection";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons";
import { utcParse } from "d3-time-format";
import humanFormat from "human-format";
import { find, forEach, round, size, sum } from "lodash-es";
import { castArray, find, forEach, round, size, sum } from "lodash-es";
export function sortRecordsByNameLabel(
record1: { name_label: string },
@@ -17,7 +21,14 @@ export function sortRecordsByNameLabel(
const label1 = record1.name_label.toLocaleLowerCase();
const label2 = record2.name_label.toLocaleLowerCase();
return label1.localeCompare(label2);
switch (true) {
case label1 < label2:
return -1;
case label1 > label2:
return 1;
default:
return 0;
}
}
export function escapeRegExp(string: string) {
@@ -103,7 +114,29 @@ export function getStatsLength(stats?: object | any[]) {
return size(find(stats, (stat) => stat != null));
}
export const buildXoObject = <T extends XenApiRecord<RawObjectType>>(
export function isHostRunning(
host: XenApiHost,
hostMetricsSubscription: Subscription<XenApiHostMetrics, object>
) {
return hostMetricsSubscription.getByOpaqueRef(host.metrics)?.live === true;
}
export function getHostMemory(
host: XenApiHost,
hostMetricsSubscription: Subscription<XenApiHostMetrics, object>
) {
const hostMetrics = hostMetricsSubscription.getByOpaqueRef(host.metrics);
if (hostMetrics !== undefined) {
const total = +hostMetrics.memory_total;
return {
usage: total - +hostMetrics.memory_free,
size: total,
};
}
}
export const buildXoObject = <T extends XenApiRecord<string>>(
record: RawXenApiRecord<T>,
params: { opaqueRef: T["$ref"] }
) => {
@@ -149,3 +182,13 @@ export function parseRamUsage(
export const getFirst = <T>(value: T | T[]): T | undefined =>
Array.isArray(value) ? value[0] : value;
export const isOperationsPending = (
obj: XenApiVm,
operations: VM_OPERATION[] | VM_OPERATION
) => {
const currentOperations = Object.values(obj.current_operations);
return castArray(operations).some((operation) =>
currentOperations.includes(operation)
);
};

View File

@@ -1,112 +0,0 @@
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type { RawTypeToRecord } from "@/types/xen-api-collection";
import { whenever } from "@vueuse/core";
import { computed, reactive } from "vue";
const collections = new Map<RawObjectType, XenApiCollection<any>>();
export const getXenApiCollection = <
ObjectType extends RawObjectType,
Record extends RawTypeToRecord<ObjectType> = RawTypeToRecord<ObjectType>,
>(
type: ObjectType
) => {
if (!collections.has(type)) {
collections.set(type, new XenApiCollection(type));
}
return collections.get(type)! as XenApiCollection<Record>;
};
export class XenApiCollection<Record extends XenApiRecord<any>> {
private state = reactive({
isReady: false,
isFetching: false,
lastError: undefined as string | undefined,
subscriptions: new Set<symbol>(),
recordsByOpaqueRef: new Map<Record["$ref"], Record>(),
recordsByUuid: new Map<Record["uuid"], Record>(),
});
isReady = computed(() => this.state.isReady);
isFetching = computed(() => this.state.isFetching);
isReloading = computed(() => this.state.isReady && this.state.isFetching);
lastError = computed(() => this.state.lastError);
hasError = computed(() => this.state.lastError !== undefined);
hasSubscriptions = computed(() => this.state.subscriptions.size > 0);
records = computed(() => Array.from(this.state.recordsByOpaqueRef.values()));
subscribe(id: symbol) {
this.state.subscriptions.add(id);
}
unsubscribe(id: symbol) {
this.state.subscriptions.delete(id);
}
constructor(private type: RawObjectType) {
const xenApiStore = useXenApiStore();
whenever(
() => xenApiStore.isConnected && this.hasSubscriptions.value,
() => this.fetchAll(xenApiStore)
);
}
getByOpaqueRef(opaqueRef: Record["$ref"]) {
return this.state.recordsByOpaqueRef.get(opaqueRef);
}
getByUuid(uuid: Record["uuid"]) {
return this.state.recordsByUuid.get(uuid);
}
hasUuid(uuid: Record["uuid"]) {
return this.state.recordsByUuid.has(uuid);
}
add(record: Record) {
this.state.recordsByOpaqueRef.set(record.$ref, record);
this.state.recordsByUuid.set(record.uuid, record);
}
update(record: Record) {
this.state.recordsByOpaqueRef.set(record.$ref, record);
this.state.recordsByUuid.set(record.uuid, record);
}
remove(opaqueRef: Record["$ref"]) {
if (!this.state.recordsByOpaqueRef.has(opaqueRef)) {
return;
}
const record = this.state.recordsByOpaqueRef.get(opaqueRef)!;
this.state.recordsByOpaqueRef.delete(opaqueRef);
this.state.recordsByUuid.delete(record.uuid);
}
private async fetchAll(xenApiStore: ReturnType<typeof useXenApiStore>) {
try {
this.state.isFetching = true;
this.state.lastError = undefined;
const records = await xenApiStore
.getXapi()
.loadRecords<any, Record>(this.type);
this.state.recordsByOpaqueRef.clear();
this.state.recordsByUuid.clear();
records.forEach((record) => this.add(record));
this.state.isReady = true;
} catch {
this.state.lastError = `[${this.type}] Failed to fetch records`;
} finally {
this.state.isFetching = false;
}
}
}

View File

@@ -1,5 +1,4 @@
import { buildXoObject, parseDateTime } from "@/libs/utils";
import type { RawTypeToRecord } from "@/types/xen-api-collection";
import { JSONRPCClient } from "json-rpc-2.0";
import { castArray } from "lodash-es";
@@ -91,17 +90,14 @@ export enum VM_OPERATION {
declare const __brand: unique symbol;
export interface XenApiRecord<Name extends RawObjectType> {
export interface XenApiRecord<Name extends string> {
$ref: string & { [__brand]: `${Name}Ref` };
uuid: string & { [__brand]: `${Name}Uuid` };
}
export type RawXenApiRecord<T extends XenApiRecord<RawObjectType>> = Omit<
T,
"$ref"
>;
export type RawXenApiRecord<T extends XenApiRecord<string>> = Omit<T, "$ref">;
export interface XenApiPool extends XenApiRecord<"pool"> {
export interface XenApiPool extends XenApiRecord<"Pool"> {
cpu_info: {
cpu_count: string;
};
@@ -109,7 +105,7 @@ export interface XenApiPool extends XenApiRecord<"pool"> {
name_label: string;
}
export interface XenApiHost extends XenApiRecord<"host"> {
export interface XenApiHost extends XenApiRecord<"Host"> {
address: string;
name_label: string;
metrics: XenApiHostMetrics["$ref"];
@@ -118,13 +114,13 @@ export interface XenApiHost extends XenApiRecord<"host"> {
software_version: { product_version: string };
}
export interface XenApiSr extends XenApiRecord<"SR"> {
export interface XenApiSr extends XenApiRecord<"Sr"> {
name_label: string;
physical_size: number;
physical_utilisation: number;
}
export interface XenApiVm extends XenApiRecord<"VM"> {
export interface XenApiVm extends XenApiRecord<"Vm"> {
current_operations: Record<string, VM_OPERATION>;
guest_metrics: string;
metrics: XenApiVmMetrics["$ref"];
@@ -139,24 +135,24 @@ export interface XenApiVm extends XenApiRecord<"VM"> {
VCPUs_at_startup: number;
}
export interface XenApiConsole extends XenApiRecord<"console"> {
export interface XenApiConsole extends XenApiRecord<"Console"> {
protocol: string;
location: string;
}
export interface XenApiHostMetrics extends XenApiRecord<"host_metrics"> {
export interface XenApiHostMetrics extends XenApiRecord<"HostMetrics"> {
live: boolean;
memory_free: number;
memory_total: number;
}
export interface XenApiVmMetrics extends XenApiRecord<"VM_metrics"> {
export interface XenApiVmMetrics extends XenApiRecord<"VmMetrics"> {
VCPUs_number: number;
}
export type XenApiVmGuestMetrics = XenApiRecord<"VM_guest_metrics">;
export type XenApiVmGuestMetrics = XenApiRecord<"VmGuestMetrics">;
export interface XenApiTask extends XenApiRecord<"task"> {
export interface XenApiTask extends XenApiRecord<"Task"> {
name_label: string;
resident_on: XenApiHost["$ref"];
created: string;
@@ -165,22 +161,17 @@ export interface XenApiTask extends XenApiRecord<"task"> {
progress: number;
}
export interface XenApiMessage<T extends RawObjectType = RawObjectType>
extends XenApiRecord<"message"> {
body: string;
cls: T;
export interface XenApiMessage extends XenApiRecord<"Message"> {
name: string;
obj_uuid: RawTypeToRecord<T>["uuid"];
priority: number;
timestamp: string;
cls: RawObjectType;
}
type WatchCallbackResult = {
id: string;
class: ObjectType;
operation: "add" | "mod" | "del";
ref: XenApiRecord<RawObjectType>["$ref"];
snapshot: RawXenApiRecord<XenApiRecord<RawObjectType>>;
ref: XenApiRecord<string>["$ref"];
snapshot: RawXenApiRecord<XenApiRecord<string>>;
};
type WatchCallback = (results: WatchCallbackResult[]) => void;
@@ -293,17 +284,16 @@ export default class XenApi {
return fetch(url, { signal: abortSignal });
}
async loadRecords<
T extends RawObjectType,
R extends RawTypeToRecord<T> = RawTypeToRecord<T>,
>(type: T): Promise<R[]> {
const result = await this.#call<{ [key: string]: R }>(
async loadRecords<T extends XenApiRecord<string>>(
type: RawObjectType
): Promise<T[]> {
const result = await this.#call<{ [key: string]: RawXenApiRecord<T> }>(
`${type}.get_all_records`,
[this.sessionId]
);
return Object.entries(result).map(([opaqueRef, record]) =>
buildXoObject(record, { opaqueRef: opaqueRef as R["$ref"] })
buildXoObject(record, { opaqueRef: opaqueRef as T["$ref"] })
);
}

View File

@@ -18,7 +18,6 @@
"community": "Community",
"community-name": "{name} community",
"console": "Console",
"console-unavailable": "Console unavailable",
"copy": "Copy",
"cpu-provisioning": "CPU provisioning",
"cpu-usage": "CPU usage",
@@ -33,7 +32,7 @@
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"edit-config": "Edit config",
"error-no-data": "Error, can't collect data.",
"error-occurred": "An error has occurred",
"error-occured": "An error has occurred",
"export": "Export",
"export-table-to": "Export table to {type}",
"export-vms": "Export VMs",
@@ -69,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",
@@ -78,11 +78,10 @@
"news": "News",
"news-name": "{name} news",
"new-features-are-coming": "New features are coming soon!",
"no-tasks": "No tasks",
"not-found": "Not found",
"no-options-available": "No options available",
"no-results-found": "No results found",
"object": "Object",
"object-not-found": "Object {id} can't be found…",
"open-in-new-window": "Open in new window",
"or": "Or",
"page-not-found": "This page is not to be found…",
"password": "Password",
@@ -91,7 +90,6 @@
"please-confirm": "Please confirm",
"pool-cpu-usage": "Pool CPU Usage",
"pool-ram-usage": "Pool RAM Usage",
"power-on-for-console": "Power on your VM to access its console",
"power-state": "Power state",
"property": "Property",
"ram-usage": "RAM usage",
@@ -115,6 +113,7 @@
"settings": "Settings",
"shutdown": "Shutdown",
"snapshot": "Snapshot",
"selected-vms-in-execution": "Some selected VMs are running",
"sort-by": "Sort by",
"stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage",
@@ -131,6 +130,7 @@
"system": "System",
"task": {
"estimated-end": "Estimated end",
"page-title": "Tasks | (1) Tasks | ({n}) Tasks",
"progress": "Progress",
"started": "Started"
},

View File

@@ -18,7 +18,6 @@
"community": "Communauté",
"community-name": "Communauté {name}",
"console": "Console",
"console-unavailable": "Console indisponible",
"copy": "Copier",
"cpu-provisioning": "Provisionnement CPU",
"cpu-usage": "Utilisation CPU",
@@ -33,7 +32,7 @@
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"edit-config": "Modifier config",
"error-no-data": "Erreur, impossible de collecter les données.",
"error-occurred": "Une erreur est survenue",
"error-occured": "Une erreur est survenue",
"export": "Exporter",
"export-table-to": "Exporter le tableau en {type}",
"export-vms": "Exporter les VMs",
@@ -69,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",
@@ -78,11 +78,10 @@
"news": "Actualités",
"news-name": "Actualités {name}",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"no-tasks": "Aucune tâche",
"not-found": "Non trouvé",
"no-options-available": "Aucune option disponible",
"no-results-found": "Aucun résultat trouvé",
"object": "Objet",
"object-not-found": "L'objet {id} est introuvable…",
"open-in-new-window": "Ouvrir dans une nouvelle fenêtre",
"or": "Ou",
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
@@ -91,7 +90,6 @@
"please-confirm": "Veuillez confirmer",
"pool-cpu-usage": "Utilisation CPU du Pool",
"pool-ram-usage": "Utilisation RAM du Pool",
"power-on-for-console": "Allumez votre VM pour accéder à sa console",
"power-state": "État d'alimentation",
"property": "Propriété",
"ram-usage": "Utilisation de la RAM",
@@ -115,6 +113,7 @@
"settings": "Paramètres",
"shutdown": "Arrêter",
"snapshot": "Instantané",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"sort-by": "Trier par",
"stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée",
@@ -131,6 +130,7 @@
"system": "Système",
"task": {
"estimated-end": "Fin estimée",
"page-title": "Tâches | (1) Tâches | ({n}) Tâches",
"progress": "Progression",
"started": "Démarré"
},

View File

@@ -33,7 +33,7 @@ const router = createRouter({
},
{
path: "/:pathMatch(.*)*",
name: "not-found",
name: "notFound",
component: () => import("@/views/PageNotFoundView.vue"),
},
],

View File

@@ -1,21 +1,21 @@
import type { RouteRecordRaw } from "vue-router";
const componentLoaders = import.meta.glob("@/stories/**/*.story.vue");
const docLoaders = import.meta.glob("@/stories/**/*.story.md", { as: "raw" });
const componentLoaders = import.meta.glob("@/stories/*.story.vue");
const docLoaders = import.meta.glob("@/stories/*.story.md", { as: "raw" });
const children: RouteRecordRaw[] = Object.entries(componentLoaders).map(
([path, componentLoader]) => {
const basePath = path.replace(/^\/src\/stories\/(.*)\.story.vue$/, "$1");
const basename = path.replace(/^\/src\/stories\/(.*)\.story.vue$/, "$1");
const docPath = path.replace(/\.vue$/, ".md");
const routeName = `story-${basePath}`;
const routeName = `story-${basename}`;
return {
name: routeName,
path: basePath,
path: basename,
component: componentLoader,
meta: {
isStory: true,
storyTitle: basePathToStoryTitle(basePath),
storyTitle: basenameToStoryTitle(basename),
storyMdLoader: docLoaders[docPath],
},
};
@@ -46,10 +46,8 @@ export default {
* Basename: `my-component`
* Page title: `My Component`
*/
function basePathToStoryTitle(basePath: string) {
return basePath
.split("/")
.pop()!
function basenameToStoryTitle(basename: string) {
return basename
.split("-")
.map((s) => `${s.charAt(0).toUpperCase()}${s.substring(1)}`)
.join(" ");

View File

@@ -0,0 +1,31 @@
import type { XenApiMessage } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { createSubscribe } from "@/types/xapi-collection";
import { defineStore } from "pinia";
import { computed } from "vue";
export const useAlarmStore = defineStore("alarm", () => {
const messageCollection = useXapiCollectionStore().get("message");
const subscribe = createSubscribe<XenApiMessage, []>((options) => {
const originalSubscription = messageCollection.subscribe(options);
const extendedSubscription = {
records: computed(() =>
originalSubscription.records.value.filter(
(record) => record.name === "alarm"
)
),
};
return {
...originalSubscription,
...extendedSubscription,
};
});
return {
...messageCollection,
subscribe,
};
});

View File

@@ -0,0 +1,6 @@
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { defineStore } from "pinia";
export const useConsoleStore = defineStore("console", () =>
useXapiCollectionStore().get("console")
);

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