Compare commits
1 Commits
fix_stats_
...
file-resto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c249c91186 |
@@ -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
|
||||
//
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"author": "reedog117",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,7 +11,6 @@ import { mount } from '@vates/fuse-vhd'
|
||||
import { readdir, lstat } from 'node:fs/promises'
|
||||
import { synchronized } from 'decorator-synchronized'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ZipFile } from 'yazl'
|
||||
import Disposable from 'promise-toolbox/Disposable'
|
||||
import fromCallback from 'promise-toolbox/fromCallback'
|
||||
import fromEvent from 'promise-toolbox/fromEvent'
|
||||
@@ -30,6 +29,7 @@ import { isValidXva } from './_isValidXva.mjs'
|
||||
import { listPartitions, LVM_PARTITION_TYPE } from './_listPartitions.mjs'
|
||||
import { lvs, pvs } from './_lvm.mjs'
|
||||
import { watchStreamSize } from './_watchStreamSize.mjs'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
|
||||
|
||||
@@ -45,23 +45,6 @@ const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createSafeReaddir = (handler, methodName) => (path, options) =>
|
||||
handler.list(path, options).catch(error => {
|
||||
if (error?.code !== 'ENOENT') {
|
||||
@@ -212,10 +195,16 @@ export class RemoteAdapter {
|
||||
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)
|
||||
// don't use --symlinks due to bug
|
||||
//
|
||||
// see https://bugs.launchpad.net/ubuntu/+source/zip/+bug/1892338
|
||||
const cp = spawn('zip', ['--quiet', '--recurse-paths', '-', ...paths.map(makeRelative)], { cwd: path })
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cp.on('error', reject).on('spawn', resolve)
|
||||
})
|
||||
|
||||
outputStream = cp.stdout
|
||||
} else {
|
||||
throw new Error('unsupported format ' + format)
|
||||
}
|
||||
|
||||
@@ -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')))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -43,8 +43,7 @@
|
||||
"tar": "^6.1.15",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.5.0",
|
||||
"xen-api": "^1.3.4",
|
||||
"yazl": "^2.5.1"
|
||||
"xen-api": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fs-extra": "^11.1.0",
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
// Keeping this file to prevent applying the global monorepo config for now
|
||||
module.exports = {
|
||||
trailingComma: "es5",
|
||||
};
|
||||
module.exports = {};
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
## **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))
|
||||
|
||||
@@ -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))
|
||||
),
|
||||
};
|
||||
};
|
||||
```
|
||||
144
@xen-orchestra/lite/docs/xen-api-record-stores.md
Normal file
144
@xen-orchestra/lite/docs/xen-api-record-stores.md
Normal 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 });
|
||||
```
|
||||
@@ -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",
|
||||
@@ -22,7 +22,7 @@
|
||||
"@types/marked": "^4.0.8",
|
||||
"@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 +49,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",
|
||||
|
||||
@@ -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";
|
||||
@@ -42,9 +42,7 @@ if (link == null) {
|
||||
link.href = favicon;
|
||||
|
||||
const xenApiStore = useXenApiStore();
|
||||
|
||||
const { pool } = usePoolCollection();
|
||||
|
||||
const { pool } = usePoolStore().subscribe();
|
||||
useChartTheme();
|
||||
const uiStore = useUiStore();
|
||||
|
||||
@@ -92,9 +90,5 @@ whenever(
|
||||
flex: 1;
|
||||
height: calc(100vh - 8rem);
|
||||
background-color: var(--background-color-secondary);
|
||||
|
||||
&.no-ui {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 63 KiB |
@@ -64,7 +64,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
@@ -140,7 +140,6 @@ const props = defineProps<{
|
||||
settings?: Record<string, any>;
|
||||
}
|
||||
>;
|
||||
fullWidthComponent?: boolean;
|
||||
}>();
|
||||
|
||||
enum TAB {
|
||||
@@ -330,10 +329,6 @@ const applyPreset = (preset: {
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
&.full-width {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -26,21 +26,21 @@ 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();
|
||||
} = useVmStore().subscribe();
|
||||
|
||||
const {
|
||||
isReady: isHostMetricsReady,
|
||||
records: hostMetrics,
|
||||
hasError: hasHostMetricsError,
|
||||
} = useHostMetricsCollection();
|
||||
} = useHostMetricsStore().subscribe();
|
||||
|
||||
const hasError = computed(() => hasVmError.value || hasHostMetricsError.value);
|
||||
|
||||
|
||||
@@ -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 }[];
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
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";
|
||||
import { useTaskStore } from "@/stores/task.store";
|
||||
|
||||
const { pendingTasks } = useTaskCollection();
|
||||
const { pendingTasks } = useTaskStore().subscribe();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
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 type { XenApiTask } from "@/libs/xen-api";
|
||||
import { useTaskStore } from "@/stores/task.store";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -49,7 +49,7 @@ const props = defineProps<{
|
||||
finishedTasks?: XenApiTask[];
|
||||
}>();
|
||||
|
||||
const { hasError, isFetching } = useTaskCollection();
|
||||
const { hasError, isFetching } = useTaskStore().subscribe();
|
||||
|
||||
const hasTasks = computed(
|
||||
() => props.pendingTasks.length > 0 || (props.finishedTasks?.length ?? 0) > 0
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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]),
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
|
||||
|
||||
export const useConsoleCollection = () => useXenApiCollection("console");
|
||||
@@ -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>,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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]
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
|
||||
|
||||
export const useSrCollection = () => useXenApiCollection("SR");
|
||||
@@ -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(),
|
||||
})),
|
||||
}
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -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>,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { useXenApiCollection } from "@/composables/xen-api-collection.composable";
|
||||
|
||||
export const useVmMetricsCollection = () => useXenApiCollection("VM_metrics");
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"] })
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -82,7 +81,6 @@
|
||||
"not-found": "Not 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 +89,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 +112,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",
|
||||
|
||||
@@ -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",
|
||||
@@ -82,7 +81,6 @@
|
||||
"not-found": "Non 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 +89,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 +112,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",
|
||||
|
||||
@@ -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(" ");
|
||||
|
||||
31
@xen-orchestra/lite/src/stores/alarm.store.ts
Normal file
31
@xen-orchestra/lite/src/stores/alarm.store.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
6
@xen-orchestra/lite/src/stores/console.store.ts
Normal file
6
@xen-orchestra/lite/src/stores/console.store.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useConsoleStore = defineStore("console", () =>
|
||||
useXapiCollectionStore().get("console")
|
||||
);
|
||||
6
@xen-orchestra/lite/src/stores/host-metrics.store.ts
Normal file
6
@xen-orchestra/lite/src/stores/host-metrics.store.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useHostMetricsStore = defineStore("host-metrics", () =>
|
||||
useXapiCollectionStore().get("host_metrics")
|
||||
);
|
||||
91
@xen-orchestra/lite/src/stores/host.store.ts
Normal file
91
@xen-orchestra/lite/src/stores/host.store.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { isHostRunning, sortRecordsByNameLabel } from "@/libs/utils";
|
||||
import type {
|
||||
GRANULARITY,
|
||||
HostStats,
|
||||
XapiStatsResponse,
|
||||
} from "@/libs/xapi-stats";
|
||||
import type { XenApiHost, XenApiHostMetrics } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import type { Subscription } from "@/types/xapi-collection";
|
||||
import { createSubscribe } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, type ComputedRef } from "vue";
|
||||
|
||||
type GetStats = (
|
||||
hostUuid: XenApiHost["uuid"],
|
||||
granularity: GRANULARITY,
|
||||
ignoreExpired: boolean,
|
||||
opts: { abortSignal?: AbortSignal }
|
||||
) => Promise<XapiStatsResponse<HostStats> | undefined> | undefined;
|
||||
|
||||
type GetStatsExtension = {
|
||||
getStats: GetStats;
|
||||
};
|
||||
|
||||
type RunningHostsExtension = [
|
||||
{ runningHosts: ComputedRef<XenApiHost[]> },
|
||||
{ hostMetricsSubscription: Subscription<XenApiHostMetrics, any> }
|
||||
];
|
||||
|
||||
type Extensions = [GetStatsExtension, RunningHostsExtension];
|
||||
|
||||
export const useHostStore = defineStore("host", () => {
|
||||
const xenApiStore = useXenApiStore();
|
||||
const hostCollection = useXapiCollectionStore().get("host");
|
||||
|
||||
hostCollection.setSort(sortRecordsByNameLabel);
|
||||
|
||||
const subscribe = createSubscribe<XenApiHost, Extensions>((options) => {
|
||||
const originalSubscription = hostCollection.subscribe(options);
|
||||
|
||||
const getStats: GetStats = (
|
||||
hostUuid,
|
||||
granularity,
|
||||
ignoreExpired = false,
|
||||
{ abortSignal }
|
||||
) => {
|
||||
const host = originalSubscription.getByUuid(hostUuid);
|
||||
|
||||
if (host === undefined) {
|
||||
throw new Error(`Host ${hostUuid} could not be found.`);
|
||||
}
|
||||
|
||||
const xapiStats = xenApiStore.isConnected
|
||||
? xenApiStore.getXapiStats()
|
||||
: undefined;
|
||||
|
||||
return xapiStats?._getAndUpdateStats<HostStats>({
|
||||
abortSignal,
|
||||
host,
|
||||
ignoreExpired,
|
||||
uuid: host.uuid,
|
||||
granularity,
|
||||
});
|
||||
};
|
||||
|
||||
const extendedSubscription = {
|
||||
getStats,
|
||||
};
|
||||
|
||||
const hostMetricsSubscription = options?.hostMetricsSubscription;
|
||||
|
||||
const runningHostsSubscription = hostMetricsSubscription !== undefined && {
|
||||
runningHosts: computed(() =>
|
||||
originalSubscription.records.value.filter((host) =>
|
||||
isHostRunning(host, hostMetricsSubscription)
|
||||
)
|
||||
),
|
||||
};
|
||||
return {
|
||||
...originalSubscription,
|
||||
...extendedSubscription,
|
||||
...runningHostsSubscription,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...hostCollection,
|
||||
subscribe,
|
||||
};
|
||||
});
|
||||
34
@xen-orchestra/lite/src/stores/pool.store.ts
Normal file
34
@xen-orchestra/lite/src/stores/pool.store.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { getFirst } from "@/libs/utils";
|
||||
import type { XenApiPool } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { createSubscribe } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, type ComputedRef } from "vue";
|
||||
|
||||
type PoolExtension = {
|
||||
pool: ComputedRef<XenApiPool | undefined>;
|
||||
};
|
||||
|
||||
type Extensions = [PoolExtension];
|
||||
|
||||
export const usePoolStore = defineStore("pool", () => {
|
||||
const poolCollection = useXapiCollectionStore().get("pool");
|
||||
|
||||
const subscribe = createSubscribe<XenApiPool, Extensions>((options) => {
|
||||
const originalSubscription = poolCollection.subscribe(options);
|
||||
|
||||
const extendedSubscription = {
|
||||
pool: computed(() => getFirst(originalSubscription.records.value)),
|
||||
};
|
||||
|
||||
return {
|
||||
...originalSubscription,
|
||||
...extendedSubscription,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...poolCollection,
|
||||
subscribe,
|
||||
};
|
||||
});
|
||||
6
@xen-orchestra/lite/src/stores/storage.store.ts
Normal file
6
@xen-orchestra/lite/src/stores/storage.store.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useSrStore = defineStore("SR", () =>
|
||||
useXapiCollectionStore().get("SR")
|
||||
);
|
||||
64
@xen-orchestra/lite/src/stores/task.store.ts
Normal file
64
@xen-orchestra/lite/src/stores/task.store.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
|
||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||
import useCollectionSorter from "@/composables/collection-sorter.composable";
|
||||
import useFilteredCollection from "@/composables/filtered-collection.composable";
|
||||
import useSortedCollection from "@/composables/sorted-collection.composable";
|
||||
import type { XenApiTask } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { createSubscribe } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
|
||||
type PendingTasksExtension = {
|
||||
pendingTasks: ComputedRef<XenApiTask[]>;
|
||||
};
|
||||
|
||||
type FinishedTasksExtension = {
|
||||
finishedTasks: Ref<XenApiTask[]>;
|
||||
};
|
||||
|
||||
type Extensions = [PendingTasksExtension, FinishedTasksExtension];
|
||||
|
||||
export const useTaskStore = defineStore("task", () => {
|
||||
const tasksCollection = useXapiCollectionStore().get("task");
|
||||
|
||||
const subscribe = createSubscribe<XenApiTask, Extensions>(() => {
|
||||
const subscription = tasksCollection.subscribe();
|
||||
|
||||
const { compareFn } = useCollectionSorter<XenApiTask>({
|
||||
initialSorts: ["-created"],
|
||||
});
|
||||
|
||||
const sortedTasks = useSortedCollection(subscription.records, compareFn);
|
||||
|
||||
const { predicate } = useCollectionFilter({
|
||||
initialFilters: [
|
||||
"!name_label:|(SR.scan host.call_plugin)",
|
||||
"status:pending",
|
||||
],
|
||||
});
|
||||
|
||||
const extendedSubscription = {
|
||||
pendingTasks: useFilteredCollection<XenApiTask>(sortedTasks, predicate),
|
||||
finishedTasks: useArrayRemovedItemsHistory(
|
||||
sortedTasks,
|
||||
(task) => task.uuid,
|
||||
{
|
||||
limit: 50,
|
||||
onRemove: (tasks) =>
|
||||
tasks.map((task) => ({
|
||||
...task,
|
||||
finished: new Date().toISOString(),
|
||||
})),
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
...extendedSubscription,
|
||||
};
|
||||
});
|
||||
|
||||
return { ...tasksCollection, subscribe };
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useBreakpoints, useColorMode } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
export const useUiStore = defineStore("ui", () => {
|
||||
const currentHostOpaqueRef = ref();
|
||||
@@ -14,14 +13,10 @@ export const useUiStore = defineStore("ui", () => {
|
||||
|
||||
const isMobile = computed(() => !isDesktop.value);
|
||||
|
||||
const route = useRoute();
|
||||
const hasUi = computed(() => route.query.ui !== "0");
|
||||
|
||||
return {
|
||||
colorMode,
|
||||
currentHostOpaqueRef,
|
||||
isDesktop,
|
||||
isMobile,
|
||||
hasUi,
|
||||
};
|
||||
});
|
||||
|
||||
6
@xen-orchestra/lite/src/stores/vm-guest-metrics.store.ts
Normal file
6
@xen-orchestra/lite/src/stores/vm-guest-metrics.store.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useVmGuestMetricsStore = defineStore("vm-guest-metrics", () =>
|
||||
useXapiCollectionStore().get("VM_guest_metrics")
|
||||
);
|
||||
6
@xen-orchestra/lite/src/stores/vm-metrics.store.ts
Normal file
6
@xen-orchestra/lite/src/stores/vm-metrics.store.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useVmMetricsStore = defineStore("vm-metrics", () =>
|
||||
useXapiCollectionStore().get("VM_metrics")
|
||||
);
|
||||
125
@xen-orchestra/lite/src/stores/vm.store.ts
Normal file
125
@xen-orchestra/lite/src/stores/vm.store.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { sortRecordsByNameLabel } from "@/libs/utils";
|
||||
import type {
|
||||
GRANULARITY,
|
||||
VmStats,
|
||||
XapiStatsResponse,
|
||||
} from "@/libs/xapi-stats";
|
||||
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
|
||||
import { POWER_STATE } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { createSubscribe, type Subscription } from "@/types/xapi-collection";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, type ComputedRef } from "vue";
|
||||
|
||||
type GetStats = (
|
||||
id: XenApiVm["uuid"],
|
||||
granularity: GRANULARITY,
|
||||
ignoreExpired: boolean,
|
||||
opts: { abortSignal?: AbortSignal }
|
||||
) => Promise<XapiStatsResponse<VmStats> | undefined> | undefined;
|
||||
type DefaultExtension = {
|
||||
recordsByHostRef: ComputedRef<Map<XenApiHost["$ref"], XenApiVm[]>>;
|
||||
runningVms: ComputedRef<XenApiVm[]>;
|
||||
};
|
||||
|
||||
type GetStatsExtension = [
|
||||
{
|
||||
getStats: GetStats;
|
||||
},
|
||||
{ hostSubscription: Subscription<XenApiHost, object> }
|
||||
];
|
||||
|
||||
type Extensions = [DefaultExtension, GetStatsExtension];
|
||||
|
||||
export const useVmStore = defineStore("vm", () => {
|
||||
const vmCollection = useXapiCollectionStore().get("VM");
|
||||
|
||||
vmCollection.setFilter(
|
||||
(vm) => !vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain
|
||||
);
|
||||
|
||||
vmCollection.setSort(sortRecordsByNameLabel);
|
||||
|
||||
const subscribe = createSubscribe<XenApiVm, Extensions>((options) => {
|
||||
const originalSubscription = vmCollection.subscribe(options);
|
||||
|
||||
const extendedSubscription = {
|
||||
recordsByHostRef: computed(() => {
|
||||
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
|
||||
|
||||
originalSubscription.records.value.forEach((vm) => {
|
||||
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
|
||||
vmsByHostOpaqueRef.set(vm.resident_on, []);
|
||||
}
|
||||
|
||||
vmsByHostOpaqueRef.get(vm.resident_on)?.push(vm);
|
||||
});
|
||||
|
||||
return vmsByHostOpaqueRef;
|
||||
}),
|
||||
runningVms: computed(() =>
|
||||
originalSubscription.records.value.filter(
|
||||
(vm) => vm.power_state === POWER_STATE.RUNNING
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
const hostSubscription = options?.hostSubscription;
|
||||
|
||||
const getStatsSubscription:
|
||||
| {
|
||||
getStats: GetStats;
|
||||
}
|
||||
| undefined =
|
||||
hostSubscription !== undefined
|
||||
? {
|
||||
getStats: (
|
||||
id,
|
||||
granularity,
|
||||
ignoreExpired = false,
|
||||
{ abortSignal }
|
||||
) => {
|
||||
const xenApiStore = useXenApiStore();
|
||||
|
||||
if (!xenApiStore.isConnected) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const vm = originalSubscription.getByUuid(id);
|
||||
|
||||
if (vm === undefined) {
|
||||
throw new Error(`VM ${id} could not be found.`);
|
||||
}
|
||||
|
||||
const host = hostSubscription.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,
|
||||
});
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...originalSubscription,
|
||||
...extendedSubscription,
|
||||
...getStatsSubscription,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...vmCollection,
|
||||
subscribe,
|
||||
};
|
||||
});
|
||||
156
@xen-orchestra/lite/src/stores/xapi-collection.store.ts
Normal file
156
@xen-orchestra/lite/src/stores/xapi-collection.store.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import type {
|
||||
RawTypeToObject,
|
||||
SubscribeOptions,
|
||||
Subscription,
|
||||
} from "@/types/xapi-collection";
|
||||
import { tryOnUnmounted, whenever } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, readonly, ref } from "vue";
|
||||
|
||||
export const useXapiCollectionStore = defineStore("xapiCollection", () => {
|
||||
const collections = ref(
|
||||
new Map<RawObjectType, ReturnType<typeof createXapiCollection<any>>>()
|
||||
);
|
||||
|
||||
function get<
|
||||
T extends RawObjectType,
|
||||
S extends XenApiRecord<string> = RawTypeToObject[T]
|
||||
>(type: T): ReturnType<typeof createXapiCollection<S>> {
|
||||
if (!collections.value.has(type)) {
|
||||
collections.value.set(type, createXapiCollection<S>(type));
|
||||
}
|
||||
|
||||
return collections.value.get(type)!;
|
||||
}
|
||||
|
||||
return { get };
|
||||
});
|
||||
|
||||
const createXapiCollection = <T extends XenApiRecord<string>>(
|
||||
type: RawObjectType
|
||||
) => {
|
||||
const isReady = ref(false);
|
||||
const isFetching = ref(false);
|
||||
const isReloading = computed(() => isReady.value && isFetching.value);
|
||||
const lastError = ref<string>();
|
||||
const hasError = computed(() => lastError.value !== undefined);
|
||||
const subscriptions = ref(new Set<symbol>());
|
||||
const recordsByOpaqueRef = ref(new Map<T["$ref"], T>());
|
||||
const recordsByUuid = ref(new Map<T["uuid"], T>());
|
||||
const filter = ref<(record: T) => boolean>();
|
||||
const sort = ref<(record1: T, record2: T) => 1 | 0 | -1>();
|
||||
const xenApiStore = useXenApiStore();
|
||||
|
||||
const setFilter = (newFilter: (record: T) => boolean) =>
|
||||
(filter.value = newFilter);
|
||||
|
||||
const setSort = (newSort: (record1: T, record2: T) => 1 | 0 | -1) =>
|
||||
(sort.value = newSort);
|
||||
|
||||
const records = computed<T[]>(() => {
|
||||
const records = Array.from(recordsByOpaqueRef.value.values()).sort(
|
||||
sort.value
|
||||
);
|
||||
return filter.value !== undefined ? records.filter(filter.value) : records;
|
||||
});
|
||||
|
||||
const getByOpaqueRef = (opaqueRef: T["$ref"]) =>
|
||||
recordsByOpaqueRef.value.get(opaqueRef);
|
||||
|
||||
const getByUuid = (uuid: T["uuid"]) => recordsByUuid.value.get(uuid);
|
||||
|
||||
const hasUuid = (uuid: T["uuid"]) => recordsByUuid.value.has(uuid);
|
||||
|
||||
const hasSubscriptions = computed(() => subscriptions.value.size > 0);
|
||||
|
||||
const fetchAll = async () => {
|
||||
try {
|
||||
isFetching.value = true;
|
||||
lastError.value = undefined;
|
||||
const records = await xenApiStore.getXapi().loadRecords<T>(type);
|
||||
recordsByOpaqueRef.value.clear();
|
||||
recordsByUuid.value.clear();
|
||||
records.forEach(add);
|
||||
isReady.value = true;
|
||||
} catch (e) {
|
||||
lastError.value = `[${type}] Failed to fetch records`;
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const add = (record: T) => {
|
||||
recordsByOpaqueRef.value.set(record.$ref, record);
|
||||
recordsByUuid.value.set(record.uuid, record);
|
||||
};
|
||||
|
||||
const update = (record: T) => {
|
||||
recordsByOpaqueRef.value.set(record.$ref, record);
|
||||
recordsByUuid.value.set(record.uuid, record);
|
||||
};
|
||||
|
||||
const remove = (opaqueRef: T["$ref"]) => {
|
||||
if (!recordsByOpaqueRef.value.has(opaqueRef)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = recordsByOpaqueRef.value.get(opaqueRef)!;
|
||||
recordsByOpaqueRef.value.delete(opaqueRef);
|
||||
recordsByUuid.value.delete(record.uuid);
|
||||
};
|
||||
|
||||
whenever(
|
||||
() => xenApiStore.isConnected && hasSubscriptions.value,
|
||||
() => fetchAll()
|
||||
);
|
||||
|
||||
function subscribe<O extends SubscribeOptions<any>>(
|
||||
options?: O
|
||||
): Subscription<T, O> {
|
||||
const id = Symbol();
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
unsubscribe(id);
|
||||
});
|
||||
|
||||
const subscription = {
|
||||
records,
|
||||
getByOpaqueRef,
|
||||
getByUuid,
|
||||
hasUuid,
|
||||
isReady: readonly(isReady),
|
||||
isFetching: readonly(isFetching),
|
||||
isReloading: isReloading,
|
||||
hasError,
|
||||
lastError: readonly(lastError),
|
||||
};
|
||||
|
||||
const start = () => subscriptions.value.add(id);
|
||||
|
||||
if (options?.immediate !== false) {
|
||||
start();
|
||||
return subscription as unknown as Subscription<T, O>;
|
||||
}
|
||||
|
||||
return {
|
||||
...subscription,
|
||||
start,
|
||||
isStarted: computed(() => subscriptions.value.has(id)),
|
||||
} as unknown as Subscription<T, O>;
|
||||
}
|
||||
|
||||
const unsubscribe = (id: symbol) => subscriptions.value.delete(id);
|
||||
|
||||
return {
|
||||
hasSubscriptions,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
setFilter,
|
||||
setSort,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useXenApiCollectionManager } from "@/composables/xen-api-collection.composable";
|
||||
import { buildXoObject } from "@/libs/utils";
|
||||
import XapiStats from "@/libs/xapi-stats";
|
||||
import XenApi, { getRawObjectType } from "@/libs/xen-api";
|
||||
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
@@ -31,11 +31,11 @@ export const useXenApiStore = defineStore("xen-api", () => {
|
||||
|
||||
xenApi.registerWatchCallBack((results) => {
|
||||
results.forEach((result) => {
|
||||
const collectionManager = useXenApiCollectionManager(
|
||||
const collection = useXapiCollectionStore().get(
|
||||
getRawObjectType(result.class)
|
||||
);
|
||||
|
||||
if (!collectionManager.hasSubscriptions.value) {
|
||||
if (!collection.hasSubscriptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@ export const useXenApiStore = defineStore("xen-api", () => {
|
||||
|
||||
switch (result.operation) {
|
||||
case "add":
|
||||
return collectionManager.add(buildObject());
|
||||
return collection.add(buildObject());
|
||||
case "mod":
|
||||
return collectionManager.update(buildObject());
|
||||
return collection.update(buildObject());
|
||||
case "del":
|
||||
return collectionManager.remove(result.ref as any);
|
||||
return collection.remove(result.ref as any);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
import RouterTab from "@/components/RouterTab.vue";
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
import { prop, setting, slot } from "@/libs/story/story-param";
|
||||
import { text } from "@/libs/story/story-widget";
|
||||
import { prop, setting, slot } from "@/libs/story/story-param.js";
|
||||
import { text } from "@/libs/story/story-widget.js";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
iconProp().preset(faRocket),
|
||||
prop('label').required().str().widget().preset('Rockets'),
|
||||
prop('count')
|
||||
.required()
|
||||
.type('string | number')
|
||||
.widget(text())
|
||||
.preset('175'),
|
||||
]"
|
||||
:presets="presets"
|
||||
>
|
||||
<UiResource v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiResource from "@/components/ui/resources/UiResource.vue";
|
||||
import { iconProp, prop } from "@/libs/story/story-param";
|
||||
import { text } from "@/libs/story/story-widget";
|
||||
import {
|
||||
faDatabase,
|
||||
faDisplay,
|
||||
faMemory,
|
||||
faMicrochip,
|
||||
faNetworkWired,
|
||||
faRocket,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const presets = {
|
||||
VMs: {
|
||||
props: {
|
||||
icon: faDisplay,
|
||||
count: 1,
|
||||
label: "VMs",
|
||||
},
|
||||
},
|
||||
vCPUs: {
|
||||
props: {
|
||||
icon: faMicrochip,
|
||||
count: 4,
|
||||
label: "vCPUs",
|
||||
},
|
||||
},
|
||||
RAM: {
|
||||
props: {
|
||||
icon: faMemory,
|
||||
count: 2,
|
||||
label: "RAM",
|
||||
},
|
||||
},
|
||||
SR: {
|
||||
props: {
|
||||
icon: faDatabase,
|
||||
count: 1,
|
||||
label: "SR",
|
||||
},
|
||||
},
|
||||
Interfaces: {
|
||||
props: {
|
||||
icon: faNetworkWired,
|
||||
count: 2,
|
||||
label: "Interfaces",
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
# Example
|
||||
|
||||
```vue-template
|
||||
<UiResources>
|
||||
<UiResource :icon="faDisplay" count="1" label="VMs" />
|
||||
<UiResource :icon="faMicrochip" count="4" label="vCPUs" />
|
||||
<UiResource :icon="faMemory" count="2" label="RAM" />
|
||||
<UiResource :icon="faDatabase" count="1" label="SR" />
|
||||
<UiResource :icon="faNetworkWired" count="2" label="Interfaces" />
|
||||
</UiResources>
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
:params="[slot().help('One or multiple `UiResource`')]"
|
||||
full-width-component
|
||||
>
|
||||
<UiResources>
|
||||
<UiResource :icon="faDisplay" count="1" label="VMs" />
|
||||
<UiResource :icon="faMicrochip" count="4" label="vCPUs" />
|
||||
<UiResource :icon="faMemory" count="2" label="RAM" />
|
||||
<UiResource :icon="faDatabase" count="1" label="SR" />
|
||||
<UiResource :icon="faNetworkWired" count="2" label="Interfaces" />
|
||||
</UiResources>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiResource from "@/components/ui/resources/UiResource.vue";
|
||||
import UiResources from "@/components/ui/resources/UiResources.vue";
|
||||
import { slot } from "@/libs/story/story-param";
|
||||
import {
|
||||
faDatabase,
|
||||
faDisplay,
|
||||
faMemory,
|
||||
faMicrochip,
|
||||
faNetworkWired,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiTab from "@/components/ui/UiTab.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
import { prop, slot } from "@/libs/story/story-param";
|
||||
import { prop, slot } from "@/libs/story/story-param.js";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -19,8 +19,8 @@
|
||||
import ComponentStory from "@/components/component-story/ComponentStory.vue";
|
||||
import UiTab from "@/components/ui/UiTab.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
import { prop, setting, slot } from "@/libs/story/story-param";
|
||||
import { text } from "@/libs/story/story-widget";
|
||||
import { prop, setting, slot } from "@/libs/story/story-param.js";
|
||||
import { text } from "@/libs/story/story-widget.js";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
140
@xen-orchestra/lite/src/types/xapi-collection.ts
Normal file
140
@xen-orchestra/lite/src/types/xapi-collection.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type {
|
||||
XenApiConsole,
|
||||
XenApiHost,
|
||||
XenApiHostMetrics,
|
||||
XenApiMessage,
|
||||
XenApiPool,
|
||||
XenApiRecord,
|
||||
XenApiSr,
|
||||
XenApiTask,
|
||||
XenApiVm,
|
||||
XenApiVmGuestMetrics,
|
||||
XenApiVmMetrics,
|
||||
} from "@/libs/xen-api";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
|
||||
type DefaultExtension<T extends XenApiRecord<string>> = {
|
||||
records: ComputedRef<T[]>;
|
||||
getByOpaqueRef: (opaqueRef: T["$ref"]) => T | undefined;
|
||||
getByUuid: (uuid: T["uuid"]) => T | undefined;
|
||||
hasUuid: (uuid: T["uuid"]) => boolean;
|
||||
isReady: Readonly<Ref<boolean>>;
|
||||
isFetching: Readonly<Ref<boolean>>;
|
||||
isReloading: ComputedRef<boolean>;
|
||||
hasError: ComputedRef<boolean>;
|
||||
lastError: Readonly<Ref<string | undefined>>;
|
||||
};
|
||||
|
||||
type DeferExtension = [
|
||||
{
|
||||
start: () => void;
|
||||
isStarted: ComputedRef<boolean>;
|
||||
},
|
||||
{ immediate: false }
|
||||
];
|
||||
|
||||
type DefaultExtensions<T extends XenApiRecord<string>> = [
|
||||
DefaultExtension<T>,
|
||||
DeferExtension
|
||||
];
|
||||
|
||||
type GenerateSubscribeOptions<Extensions extends any[]> = Extensions extends [
|
||||
infer FirstExtension,
|
||||
...infer RestExtension
|
||||
]
|
||||
? FirstExtension extends [object, infer FirstCondition]
|
||||
? FirstCondition & GenerateSubscribeOptions<RestExtension>
|
||||
: GenerateSubscribeOptions<RestExtension>
|
||||
: object;
|
||||
|
||||
export type SubscribeOptions<Extensions extends any[]> = Partial<
|
||||
GenerateSubscribeOptions<Extensions> &
|
||||
GenerateSubscribeOptions<DefaultExtensions<any>>
|
||||
>;
|
||||
|
||||
type GenerateSubscription<
|
||||
Options extends object,
|
||||
Extensions extends any[]
|
||||
> = Extensions extends [infer FirstExtension, ...infer RestExtension]
|
||||
? FirstExtension extends [infer FirstObject, infer FirstCondition]
|
||||
? Options extends FirstCondition
|
||||
? FirstObject & GenerateSubscription<Options, RestExtension>
|
||||
: GenerateSubscription<Options, RestExtension>
|
||||
: FirstExtension & GenerateSubscription<Options, RestExtension>
|
||||
: object;
|
||||
|
||||
export type Subscription<
|
||||
T extends XenApiRecord<string>,
|
||||
Options extends object,
|
||||
Extensions extends any[] = []
|
||||
> = GenerateSubscription<Options, Extensions> &
|
||||
GenerateSubscription<Options, DefaultExtensions<T>>;
|
||||
|
||||
export function createSubscribe<
|
||||
T extends XenApiRecord<string>,
|
||||
Extensions extends any[],
|
||||
Options extends object = SubscribeOptions<Extensions>
|
||||
>(builder: (options?: Options) => Subscription<T, Options, Extensions>) {
|
||||
return function subscribe<O extends Options>(
|
||||
options?: O
|
||||
): Subscription<T, O, Extensions> {
|
||||
return builder(options);
|
||||
};
|
||||
}
|
||||
|
||||
export type RawTypeToObject = {
|
||||
Bond: never;
|
||||
Certificate: never;
|
||||
Cluster: never;
|
||||
Cluster_host: never;
|
||||
DR_task: never;
|
||||
Feature: never;
|
||||
GPU_group: never;
|
||||
PBD: never;
|
||||
PCI: never;
|
||||
PGPU: never;
|
||||
PIF: never;
|
||||
PIF_metrics: never;
|
||||
PUSB: never;
|
||||
PVS_cache_storage: never;
|
||||
PVS_proxy: never;
|
||||
PVS_server: never;
|
||||
PVS_site: never;
|
||||
SDN_controller: never;
|
||||
SM: never;
|
||||
SR: XenApiSr;
|
||||
USB_group: never;
|
||||
VBD: never;
|
||||
VBD_metrics: never;
|
||||
VDI: never;
|
||||
VGPU: never;
|
||||
VGPU_type: never;
|
||||
VIF: never;
|
||||
VIF_metrics: never;
|
||||
VLAN: never;
|
||||
VM: XenApiVm;
|
||||
VMPP: never;
|
||||
VMSS: never;
|
||||
VM_guest_metrics: XenApiVmGuestMetrics;
|
||||
VM_metrics: XenApiVmMetrics;
|
||||
VUSB: never;
|
||||
blob: never;
|
||||
console: XenApiConsole;
|
||||
crashdump: never;
|
||||
host: XenApiHost;
|
||||
host_cpu: never;
|
||||
host_crashdump: never;
|
||||
host_metrics: XenApiHostMetrics;
|
||||
host_patch: never;
|
||||
message: XenApiMessage;
|
||||
network: never;
|
||||
network_sriov: never;
|
||||
pool: XenApiPool;
|
||||
pool_patch: never;
|
||||
pool_update: never;
|
||||
role: never;
|
||||
secret: never;
|
||||
subject: never;
|
||||
task: XenApiTask;
|
||||
tunnel: never;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user