Compare commits

...

55 Commits

Author SHA1 Message Date
Gabriel Gunullu
2cb986b1a3 feat(xapi/VDI_importContent): add SR name_label to task name_label (#6979) 2023-10-10 10:43:08 +02:00
Julien Fontanet
b86cb12649 chore(yarn.lock): update dev deps 2023-10-09 17:06:54 +02:00
Julien Fontanet
2af74008b2 feat(xo-server-backup-reports): errors are logged as XO tasks 2023-10-09 09:35:24 +02:00
Julien Fontanet
2e689592f1 feat(xo-server-backup-reports): error when transports not enabled 2023-10-09 09:35:24 +02:00
Julien Fontanet
3f8436b58b fix(xo-server/authenticateUser): use clearLogOnSuccess
This fixes success logs not deleted due to race conditions.
2023-10-09 09:35:24 +02:00
Julien Fontanet
e3dd59d684 feat(mixins/Tasks#create): clearLogOnSuccess option 2023-10-09 09:35:24 +02:00
mathieuRA
549d9b70a9 feat(xo-web/host): allow to force smartReboot 2023-10-06 16:52:26 +02:00
mathieuRA
3bf6aae103 feat(xapi/host_smartReboot): ability to bypass blocked operations 2023-10-06 16:52:26 +02:00
Julien Fontanet
afb110c473 fix(fs/rmtree): fix huge memory usage (#7073)
Fixes zammad#15258

This adds a sane concurrency limit of 2 per depth level.

Co-authored-by: Florent BEAUCHAMP <florent.beauchamp@vates.fr>
2023-10-06 09:52:11 +02:00
Pierre Donias
8727c3cf96 docs(patches): update URLs that need to be accessible from XOA (#7075) 2023-10-05 09:45:50 +02:00
Julien Fontanet
b13302ddeb fix(xen-api/cli): dont run default export when imported by ESM
Fix a bug in `@xen-orchestra/xapi` introduced by c3e0308ad

`module.parent` is `null` when the module is the entry point but `undefined` when imported via ESM.
2023-10-04 10:06:17 +02:00
Julien Fontanet
e89ed06314 docs(installation): Node 18 required
XO is not compatible with Node > 18 for the moment, as Node 20 will
likely graduate to LTS soon, the docs must explicitly recommend 18.
2023-10-04 09:25:37 +02:00
Malcolm Scott
e3f57998f7 fix(signin): try to preserve current page across reauthentication (#7013)
If an authentication session expires or is lost for whatever reason, XO redirects to `/signin`.  This redirect generally preserves the URL fragment (hash) which contains the page selected prior to reauthentication, i.e. if the user had been in settings/servers just beforehand, they end up at `/signin#settings/servers`.  However, currently when they log back in they end up on the home page; the page they were on is forgotten.

This commit tries to send the user back to the page they were viewing before reauthentication, by preserving the URL fragment in the login form action / by appending it to the links to authentication plugins.  (Not all authentication plugins will necessarily preserve it internally, but we can optimistically try it and see; at worst the old behaviour will remain.)
2023-10-03 12:39:57 +02:00
Julien Fontanet
8cdb5ee31b chore: update dev deps 2023-10-03 11:24:51 +02:00
Pierre Donias
5b734db656 feat(lite): 0.1.4 (#7068) 2023-10-03 10:26:05 +02:00
rbarhtaoui
e853f9d04f feat(lite): display loading icon and error message when data is not fetched (#6775) 2023-10-03 10:03:44 +02:00
Mathieu
2a5e09719e feat(lite/login): add remember me checkbox (#7030) 2023-10-03 10:01:07 +02:00
Pierre Donias
3c0477e0da feat: release 5.87.0 (#7064) 2023-09-29 11:35:23 +02:00
Pierre Donias
060d1c5297 feat: technical release (#7063) 2023-09-29 10:01:45 +02:00
Julien Fontanet
55dd7bfb9c feat(backups): don't snapshot migrating VMs
Related to zammad#16108
2023-09-28 17:42:43 +02:00
Julien Fontanet
b00cf13029 feat(backups): block snapshot migration during backup
Related to zammad#16108
2023-09-28 17:42:43 +02:00
Julien Fontanet
73755e4ccf feat(xo-server/authenticateUser): log failed attempts
Related to zammad#16318
2023-09-28 17:38:57 +02:00
Julien Fontanet
a1bd96da6a feat(mixins/Tasks#create): allow any properties 2023-09-28 17:38:57 +02:00
mathieuRA
0e934c1413 feat(xo-web/host/advanced): display system disks health 2023-09-28 17:14:09 +02:00
Florent BEAUCHAMP
eb69234a8e feat(xo-server/host): implement smartctl api call 2023-09-28 17:14:09 +02:00
mathieuRA
7659d9c0be fix(xo-web/host/advanced): catch error for ACLs users on hyper threading plugin
it broke the componentDidMount methode and didn't update the state correctly
2023-09-28 17:14:09 +02:00
Florent BEAUCHAMP
2ba81d55f8 fix(vhd-lib/test): collision during tests (#7062)
multiple tests use the same temporary files
2023-09-28 16:49:00 +02:00
Gabriel Gunullu
2e1abad255 feat(xapi/VDI_importContent): add SR name_label to task name_label (#6979) 2023-09-28 16:10:29 +02:00
Julien Fontanet
c7d5b4b063 fix(xo-web/messages): clarify *forget tokens* description
Introduced by c7df11cc6
2023-09-28 15:41:10 +02:00
Julien Fontanet
cc5f4b0996 fix(xo-web/messages): connection token → authentication token
Uniformize naming.
2023-09-28 15:41:06 +02:00
Julien Fontanet
55f627ed83 chore: fix formatting
Introduced by 869f7ffab
2023-09-28 15:37:45 +02:00
Florent BEAUCHAMP
988179a3f0 fix(xo-server): add mbr for cloud-init only for windows VM (#7050)
Fixes zammad#16808
2023-09-28 09:09:13 +02:00
Julien Fontanet
ce617e0732 fix(xo-server/host.restart): make force defaults to false
Introduced by 5ee11c7b6
2023-09-27 17:39:10 +02:00
Florent BEAUCHAMP
f0f429a473 fix(xo-server-backup-report): send report for Mirror Backup (#7049) 2023-09-27 16:39:27 +02:00
Thierry Goettelmann
bb6e158301 feat(lite): host patches (#6709) 2023-09-27 11:44:03 +02:00
Pierre Donias
7ff304a042 feat: technical release (#7058) 2023-09-27 11:30:16 +02:00
Julien Fontanet
7df1994d7f fix(xo-server/sr.getAllUnhealthyVdiChainsLength): require admin permission
Introduced by 0975863d9
2023-09-27 10:37:30 +02:00
Mathieu
a3a2fda157 feat(lite/pool/VMs): ability to snapshot selected VMs (#7021) 2023-09-26 17:28:15 +02:00
Thierry Goettelmann
d8530f9518 chore(lite): update changelog (#7057)
Fixes [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040)
2023-09-26 17:13:29 +02:00
Thierry Goettelmann
d3062ac35c feat(lite/pool/VMs): ability to migrate selected VMs (#7040) 2023-09-26 17:06:00 +02:00
Thierry Goettelmann
b11f11f4db feat(lite): rework modal system (#6994) 2023-09-26 16:25:23 +02:00
Thierry Goettelmann
79d48f3b56 feat(lite/xapi): update XenApi types and enums (#7018) 2023-09-26 15:19:33 +02:00
Pierre Donias
869f7ffab0 feat(xo-web/XOA/Support): button to restart xo-server service (#7056) 2023-09-26 14:35:17 +02:00
Julien Fontanet
6665d6a8e6 chore: format with Prettier 2023-09-26 14:34:47 +02:00
Pierre Donias
8eb0bdbda7 feat(xo-server,xo-web/SR): reclaim space (#7054)
Fixes #1204
2023-09-26 14:21:43 +02:00
Mathieu
710689db0b feat(xo-web/home/host,pool): display product brand and version (#7027) 2023-09-26 11:16:08 +02:00
mathieuRA
801eea7e75 feat(xo-web/host/advanced): confirmation modal for download system logs 2023-09-26 11:10:22 +02:00
Julien Fontanet
7885e1e6e7 feat(xo-web/host/advanced): button do download system logs
Fixes #3968
2023-09-26 11:10:22 +02:00
Julien Fontanet
d384c746ca feat(xo-server/rest-api): export host audit and system logs
See #3968
2023-09-26 11:10:22 +02:00
Pierre Donias
a30d962b1d feat(xo-server,xo-web/patching): support new XS Updates system (#7044)
See Zammad#13416

Support for new XenServer Updates system with authentication:
- User downloads Client ID JSON file from XenServer account
- User uploads it to XO in their user preferences
- XO uses `username` and `apikey` from that file to authenticate and download updates
2023-09-26 10:29:07 +02:00
Pierre Donias
b6e078716b docs(users/auth): update GitHub plugin screenshots (#7035) 2023-09-25 16:10:00 +02:00
Julien Fontanet
34b69c7ee8 chore: refresh yarn.lock
Introduced by 90e0f2684
2023-09-25 09:10:54 +02:00
Julien Fontanet
70bf8d9620 fix(xo-web/kubernetes): handle empty searches domain field
Do not send `['']` if empty.
2023-09-25 09:08:33 +02:00
Florent BEAUCHAMP
c8bfda9cf5 fix(xo-vmdk-to-vhd): handle ova with disk position collision (#7051)
Some OVA have multiple disks with the same position, which prevent the VM from being created (error while creating VBD). Renumeroting the problematic disk works around the issue.

This may lead to unbootable VM in case the renumeroted disk was the bootable one (VMware-VirtualSAN-Witness-7.0.0-15843807.ova for example).

Fixes #7046
2023-09-22 11:44:12 +02:00
Gabriel Gunullu
1eb4c20844 fix(xo-web/kubernetes): remove required property from search domain (#7028)
Make this field optional for the cluster creation.
2023-09-22 09:46:13 +02:00
153 changed files with 4828 additions and 1978 deletions

View File

@@ -13,12 +13,15 @@ describe('decorateWith', () => {
const expectedFn = Function.prototype
const newFn = () => {}
const decorator = decorateWith(function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
const decorator = decorateWith(
function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
return newFn
}, ...expectedArgs)
return newFn
},
...expectedArgs
)
const descriptor = {
configurable: true,

View File

@@ -22,7 +22,7 @@
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.5.0"
"vhd-lib": "^4.6.1"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -1,7 +1,7 @@
import assert from 'node:assert'
import { Socket } from 'node:net'
import { connect } from 'node:tls'
import { fromCallback, pRetry, pDelay, pTimeout } from 'promise-toolbox'
import { fromCallback, pRetry, pDelay, pTimeout, pFromCallback } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import { createLogger } from '@xen-orchestra/log'
@@ -112,18 +112,22 @@ export default class NbdClient {
}
async disconnect() {
warn('will try to disconnect', { serverAddress: this.#serverAddress })
if (!this.#connected) {
warn('was already disconnected', { serverAddress: this.#serverAddress })
return
}
warn('will really disconnect', { serverAddress: this.#serverAddress })
const buffer = Buffer.alloc(28)
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
buffer.writeInt16BE(0, 4) // no command flags for a disconnect
buffer.writeInt16BE(NBD_CMD_DISC, 6) // we want to disconnect from nbd server
await this.#write(buffer)
await this.#serverSocket.destroy()
warn('will send end buffer', { serverAddress: this.#serverAddress })
this.#connected = false // optimistically mark as disconnected to ensure we don' send another disconnection while handling this one
await pFromCallback(cb => this.#serverSocket.end(buffer, cb))
warn('end buffer sent', { serverAddress: this.#serverAddress })
this.#serverSocket = undefined
this.#connected = false
}
#clearReconnectPromise = () => {

View File

@@ -7,7 +7,7 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.42.0",
"@xen-orchestra/backups": "^0.43.0",
"@xen-orchestra/fs": "^4.1.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.12",
"version": "1.0.13",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -31,6 +31,11 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
throw new Error('cannot backup a VM created by this very job')
}
const currentOperations = Object.values(vm.current_operations)
if (currentOperations.some(_ => _ === 'migrate_send' || _ === 'pool_migrate')) {
throw new Error('cannot backup a VM currently being migrated')
}
this.config = config
this.job = job
this.remoteAdapters = remoteAdapters
@@ -256,7 +261,15 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
}
if (this._writers.size !== 0) {
await this._copy()
const { pool_migrate = null, migrate_send = null } = this._exportedVm.blocked_operations
const reason = 'VM migration is blocked during backup'
await this._exportedVm.update_blocked_operations({ pool_migrate: reason, migrate_send: reason })
try {
await this._copy()
} finally {
await this._exportedVm.update_blocked_operations({ pool_migrate, migrate_send })
}
}
} finally {
if (startAfter) {

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.42.0",
"version": "0.43.0",
"engines": {
"node": ">=14.18"
},
@@ -44,7 +44,7 @@
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"vhd-lib": "^4.6.1",
"xen-api": "^1.3.6",
"yazl": "^2.5.1"
},
@@ -56,7 +56,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^3.1.0"
"@xen-orchestra/xapi": "^3.2.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -624,14 +624,18 @@ export default class RemoteHandlerAbstract {
const files = await this._list(dir)
await asyncEach(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
})
this._unlink(`${dir}/${file}`).catch(
error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
},
// real unlink concurrency will be 2**max directory depth
{ concurrency: 2 }
)
)
return this._rmtree(dir)
}

View File

@@ -2,6 +2,13 @@
## **next**
## **0.1.4** (2023-10-03)
- Ability to migrate selected VMs to another host (PR [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040))
- Ability to snapshot selected VMs (PR [#7021](https://github.com/vatesfr/xen-orchestra/pull/7021))
- Add Patches to Pool Dashboard (PR [#6709](https://github.com/vatesfr/xen-orchestra/pull/6709))
- Add remember me checkbox on the login page (PR [#7030](https://github.com/vatesfr/xen-orchestra/pull/7030))
## **0.1.3** (2023-09-01)
- Add Alarms to Pool Dashboard (PR [#6976](https://github.com/vatesfr/xen-orchestra/pull/6976))

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.3",
"version": "0.1.4",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",

View File

@@ -16,6 +16,10 @@
required
/>
</FormInputWrapper>
<label class="remember-me-label">
<FormCheckbox v-model="rememberMe" />
<p>{{ $t("keep-me-logged") }}</p>
</label>
<UiButton type="submit" :busy="isConnecting">
{{ $t("login") }}
</UiButton>
@@ -28,6 +32,9 @@ import { usePageTitleStore } from "@/stores/page-title.store";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useLocalStorage } from "@vueuse/core";
import FormCheckbox from "@/components/form/FormCheckbox.vue";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import UiButton from "@/components/ui/UiButton.vue";
@@ -42,12 +49,16 @@ const password = ref("");
const error = ref<string>();
const passwordRef = ref<InstanceType<typeof FormInput>>();
const isInvalidPassword = ref(false);
const rememberMe = useLocalStorage("rememberMe", false);
const focusPasswordInput = () => passwordRef.value?.focus();
onMounted(() => {
xenApiStore.reconnect();
focusPasswordInput();
if (rememberMe.value) {
xenApiStore.reconnect();
} else {
focusPasswordInput();
}
});
watch(password, () => {
@@ -72,6 +83,19 @@ async function handleSubmit() {
</script>
<style lang="postcss" scoped>
.remember-me-label {
cursor: pointer;
width: fit-content;
& .form-checkbox {
margin: 1rem 1rem 1rem 0;
vertical-align: middle;
}
& p {
display: inline;
vertical-align: middle;
}
}
.form-container {
display: flex;
align-items: center;
@@ -87,7 +111,6 @@ form {
font-size: 2rem;
min-width: 30em;
max-width: 100%;
align-items: center;
flex-direction: column;
justify-content: center;
margin: 0 auto;
@@ -104,7 +127,7 @@ h1 {
img {
width: 40rem;
margin-bottom: 5rem;
margin: auto auto 5rem auto;
}
input {
@@ -118,6 +141,6 @@ input {
}
button {
margin-top: 2rem;
margin: 2rem auto;
}
</style>

View File

@@ -14,66 +14,66 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faFilter"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ property }}
</UiBadge>
</div>
</div>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
>
{{ property }}
</UiBadge>
</div>
</div>
</template>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
import type { Filters, NewFilter } from "@/types/filter";
import { faFilter, faPlus } from "@fortawesome/free-solid-svg-icons";
import CollectionFilterRow from "@/components/CollectionFilterRow.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiBadge from "@/components/ui/UiBadge.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { getFilterIcon } from "@/libs/utils";
import type { Filters, NewFilter } from "@/types/filter";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
defineProps<{
activeFilters: string[];
@@ -85,7 +85,7 @@ const emit = defineEmits<{
(event: "removeFilter", filter: string): void;
}>();
const { isOpen, open, close } = useModal();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const newFilters = ref<NewFilter[]>([]);
let newFilterId = 0;
@@ -156,11 +156,6 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>
@@ -190,4 +185,10 @@ const handleCancel = () => {
margin-top: 0.6rem;
gap: 0.5rem;
}
.rows {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -219,7 +219,6 @@ const valueInputAfter = computed(() =>
.collection-filter-row {
display: flex;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid var(--background-color-secondary);
gap: 1rem;
@@ -242,4 +241,8 @@ const valueInputAfter = computed(() =>
.form-widget-advanced {
flex: 1;
}
.ui-action-button:first-of-type {
margin-left: auto;
}
</style>

View File

@@ -17,56 +17,56 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faSort"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
</template>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import FormWidget from "@/components/FormWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import type { ActiveSorts, Sorts } from "@/types/sort";
import {
faCaretDown,
faCaretUp,
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { ref } from "vue";
@@ -81,7 +81,7 @@ const emit = defineEmits<{
(event: "removeSort", property: string): void;
}>();
const { isOpen, open, close } = useModal();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const newSortProperty = ref();
const newSortIsAscending = ref<boolean>(true);
@@ -96,11 +96,6 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>

View File

@@ -28,13 +28,9 @@
</tr>
</thead>
<tbody>
<tr v-for="item in filteredAndSortedCollection" :key="item[idProperty]">
<tr v-for="item in filteredAndSortedCollection" :key="item.$ref">
<td v-if="isSelectable">
<input
v-model="selected"
:value="item[props.idProperty]"
type="checkbox"
/>
<input v-model="selected" :value="item.$ref" type="checkbox" />
</td>
<slot :item="item" name="body-row" />
</tr>
@@ -42,10 +38,7 @@
</UiTable>
</template>
<script lang="ts" setup>
import { computed, toRef, watch } from "vue";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
<script generic="T extends XenApiRecord<any>" lang="ts" setup>
import CollectionFilter from "@/components/CollectionFilter.vue";
import CollectionSorter from "@/components/CollectionSorter.vue";
import UiTable from "@/components/ui/UiTable.vue";
@@ -54,17 +47,20 @@ import useCollectionSorter from "@/composables/collection-sorter.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useMultiSelect from "@/composables/multi-select.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiRecord } from "@/libs/xen-api/xen-api.types";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
import { computed, toRef, watch } from "vue";
const props = defineProps<{
modelValue?: string[];
modelValue?: T["$ref"][];
availableFilters?: Filters;
availableSorts?: Sorts;
collection: Record<string, any>[];
idProperty: string;
collection: T[];
}>();
const emit = defineEmits<{
(event: "update:modelValue", selectedRefs: string[]): void;
(event: "update:modelValue", selectedRefs: T["$ref"][]): void;
}>();
const isSelectable = computed(() => props.modelValue !== undefined);
@@ -85,12 +81,10 @@ const filteredAndSortedCollection = useSortedCollection(
compareFn
);
const usableRefs = computed(() =>
props.collection.map((item) => item[props.idProperty])
);
const usableRefs = computed(() => props.collection.map((item) => item["$ref"]));
const selectableRefs = computed(() =>
filteredAndSortedCollection.value.map((item) => item[props.idProperty])
filteredAndSortedCollection.value.map((item) => item["$ref"])
);
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs);

View File

@@ -0,0 +1,71 @@
<template>
<UiCardSpinner v-if="!areSomeLoaded" />
<UiTable v-else class="hosts-patches-table" :class="{ desktop: isDesktop }">
<tr v-for="patch in sortedPatches" :key="patch.$id">
<th>{{ patch.name }}</th>
<td>
<div class="version">
{{ patch.version }}
<template v-if="hasMultipleHosts">
<UiSpinner v-if="!areAllLoaded" />
<UiCounter
v-else
v-tooltip="{
placement: 'left',
content: $t('n-hosts-awaiting-patch', {
n: patch.$hostRefs.size,
}),
}"
:value="patch.$hostRefs.size"
class="counter"
color="error"
/>
</template>
</div>
</td>
</tr>
</UiTable>
</template>
<script lang="ts" setup>
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTable from "@/components/ui/UiTable.vue";
import type { XenApiPatchWithHostRefs } from "@/composables/host-patches.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import { useUiStore } from "@/stores/ui.store";
import { computed } from "vue";
const props = defineProps<{
patches: XenApiPatchWithHostRefs[];
hasMultipleHosts: boolean;
areAllLoaded: boolean;
areSomeLoaded: boolean;
}>();
const sortedPatches = computed(() =>
[...props.patches].sort(
(patch1, patch2) => patch1.changelog.date - patch2.changelog.date
)
);
const { isDesktop } = useUiStore();
</script>
<style lang="postcss" scoped>
.hosts-patches-table.desktop {
max-width: 45rem;
}
.version {
display: flex;
gap: 1rem;
justify-content: flex-end;
align-items: center;
}
.counter {
font-size: 1rem;
}
</style>

View File

@@ -66,8 +66,8 @@ onUnmounted(() => {
store.value?.unsubscribe(subscriptionId);
});
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(() =>
store.value?.getByUuid(props.uuid as any)
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(
() => store.value?.getByUuid(props.uuid as any)
);
const isReady = computed(() => {

View File

@@ -4,7 +4,7 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import {
faMoon,
faPause,
@@ -15,14 +15,14 @@ import {
import { computed } from "vue";
const props = defineProps<{
state: POWER_STATE;
state: VM_POWER_STATE;
}>();
const icons = {
[POWER_STATE.RUNNING]: faPlay,
[POWER_STATE.PAUSED]: faPause,
[POWER_STATE.SUSPENDED]: faMoon,
[POWER_STATE.HALTED]: faStop,
[VM_POWER_STATE.RUNNING]: faPlay,
[VM_POWER_STATE.PAUSED]: faPause,
[VM_POWER_STATE.SUSPENDED]: faMoon,
[VM_POWER_STATE.HALTED]: faStop,
};
const icon = computed(() => icons[props.state] ?? faQuestion);

View File

@@ -1,45 +1,58 @@
<template>
<UiModal
v-if="isSslModalOpen"
:icon="faServer"
color="error"
@close="clearUnreachableHostsUrls"
>
<template #title>{{ $t("unreachable-hosts") }}</template>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
</template>
<UiModal v-model="isSslModalOpen" color="error">
<ConfirmModalLayout :icon="faServer">
<template #title>{{ $t("unreachable-hosts") }}</template>
<template #default>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
</template>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="closeSslModal">{{ $t("cancel") }}</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import useModal from "@/composables/modal.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
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 { ref, watch } from "vue";
const { records: hosts } = useHostCollection();
const unreachableHostsUrls = ref<Set<string>>(new Set());
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);
const reload = () => window.location.reload();
const { isOpen: isSslModalOpen, close: closeSslModal } = useModal({
onClose: () => unreachableHostsUrls.value.clear(),
});
watch(
() => unreachableHostsUrls.value.size,
(size) => {
isSslModalOpen.value = size > 0;
},
{ immediate: true }
);
watch(hosts, (nextHosts, previousHosts) => {
difference(nextHosts, previousHosts).forEach((host) => {
const url = new URL("http://localhost");
@@ -53,7 +66,11 @@ watch(hosts, (nextHosts, previousHosts) => {
</script>
<style lang="postcss" scoped>
.description p {
margin: 1rem 0;
.description {
text-align: center;
p {
margin: 1rem 0;
}
}
</style>

View File

@@ -2,12 +2,7 @@
```vue
<template>
<LinearChart
title="Chart title"
subtitle="Chart subtitle"
:data="data"
:value-formatter="customValueFormatter"
/>
<LinearChart :data="data" :value-formatter="customValueFormatter" />
</template>
<script lang="ts" setup>

View File

@@ -1,12 +1,8 @@
<template>
<UiCard class="linear-chart">
<VueCharts :option="option" autoresize class="chart" />
<slot name="summary" />
</UiCard>
<VueCharts :option="option" autoresize class="chart" />
</template>
<script lang="ts" setup>
import UiCard from "@/components/ui/UiCard.vue";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_CHART_VALUE_FORMATTER } from "@/types/injection-keys";
import { utcFormat } from "d3-time-format";
@@ -15,7 +11,6 @@ import { LineChart } from "echarts/charts";
import {
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from "echarts/components";
import { use } from "echarts/core";
@@ -26,8 +21,6 @@ import VueCharts from "vue-echarts";
const Y_AXIS_MAX_VALUE = 200;
const props = defineProps<{
title?: string;
subtitle?: string;
data: LinearChartData;
valueFormatter?: ValueFormatter;
maxValue?: number;
@@ -52,15 +45,10 @@ use([
LineChart,
GridComponent,
TooltipComponent,
TitleComponent,
LegendComponent,
]);
const option = computed<EChartsOption>(() => ({
title: {
text: props.title,
subtext: props.subtitle,
},
legend: {
data: props.data.map((series) => series.label),
},

View File

@@ -58,7 +58,7 @@ const getDefaultOpenedDirectories = (): Set<string> => {
}
const openedDirectories = new Set<string>();
const parts = currentRoute.path.split("/");
const parts = currentRoute.path.split("/").slice(2);
let currentPath = "";
for (const part of parts) {

View File

@@ -1,6 +1,8 @@
<template>
<UiModal v-if="isRawValueModalOpen" @close="closeRawValueModal">
<CodeHighlight :code="rawValueModalPayload" />
<UiModal v-model="isRawValueModalOpen">
<BasicModalLayout>
<CodeHighlight :code="rawValueModalPayload" />
</BasicModalLayout>
</UiModal>
<StoryParamsTable>
<thead>
@@ -99,7 +101,8 @@ import CodeHighlight from "@/components/CodeHighlight.vue";
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
import StoryWidget from "@/components/component-story/StoryWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import useModal from "@/composables/modal.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
@@ -130,7 +133,6 @@ const model = useVModel(props, "modelValue", emit);
const {
open: openRawValueModal,
close: closeRawValueModal,
isOpen: isRawValueModalOpen,
payload: rawValueModalPayload,
} = useModal<string>();

View File

@@ -4,7 +4,7 @@
v-if="label !== undefined || learnMoreUrl !== undefined"
class="label-container"
>
<label :for="id" class="label">
<label :class="{ light }" :for="id" class="label">
<UiIcon :icon="icon" />
{{ label }}
</label>
@@ -58,6 +58,7 @@ const props = withDefaults(
error?: string;
help?: string;
disabled?: boolean;
light?: boolean;
}>(),
{ disabled: undefined }
);
@@ -95,14 +96,24 @@ useContext(DisabledContext, () => props.disabled);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.label {
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
font-size: 1.4rem;
padding: 1rem 0;
&.light {
font-size: 1.6rem;
color: var(--color-blue-scale-300);
font-weight: 400;
}
&:not(.light) {
font-size: 1.4rem;
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
}
}
.messages-container {

View File

@@ -1,20 +1,28 @@
<template>
<UiModal
@submit.prevent="saveJson"
v-model="isCodeModalOpen"
:color="isJsonValid ? 'success' : 'error'"
v-if="isCodeModalOpen"
:icon="faCode"
@close="closeCodeModal"
closable
>
<FormTextarea class="modal-textarea" v-model="editedJson" />
<template #buttons>
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
<UiButton :disabled="!isJsonValid" type="submit"
>{{ $t("save") }}
</UiButton>
</template>
<FormModalLayout @submit.prevent="saveJson" :icon="faCode">
<template #default>
<FormTextarea class="modal-textarea" v-model="editedJson" />
</template>
<template #buttons>
<UiButton transparent @click="formatJson">
{{ $t("reformat") }}
</UiButton>
<UiButton outlined @click="closeCodeModal">
{{ $t("cancel") }}
</UiButton>
<UiButton :disabled="!isJsonValid" type="submit">
{{ $t("save") }}
</UiButton>
</template>
</FormModalLayout>
</UiModal>
<FormInput
@click="openCodeModal"
:model-value="jsonValue"
@@ -26,8 +34,9 @@
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import FormTextarea from "@/components/form/FormTextarea.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { useVModel, whenever } from "@vueuse/core";

View File

@@ -41,11 +41,11 @@ import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
import { percent } from "@/libs/utils";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
const ACTIVE_STATES = new Set([POWER_STATE.RUNNING, POWER_STATE.PAUSED]);
const ACTIVE_STATES = new Set([VM_POWER_STATE.RUNNING, VM_POWER_STATE.PAUSED]);
const {
hasError: hostStoreHasError,

View File

@@ -0,0 +1,41 @@
<template>
<UiCard>
<UiCardTitle class="patches-title">
{{ $t("patches") }}
<template v-if="areAllLoaded" #right>
{{ $t("n-missing", { n: count }) }}
</template>
</UiCardTitle>
<div class="table-container">
<HostPatches
:are-all-loaded="areAllLoaded"
:are-some-loaded="areSomeLoaded"
:has-multiple-hosts="hosts.length > 1"
:patches="patches"
/>
</div>
</UiCard>
</template>
<script lang="ts" setup>
import HostPatches from "@/components/HostPatchesTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostPatches } from "@/composables/host-patches.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { records: hosts } = useHostCollection();
const { count, patches, areSomeLoaded, areAllLoaded } = useHostPatches(hosts);
</script>
<style lang="postcss" scoped>
.patches-title {
--section-title-right-color: var(--color-red-vates-base);
}
.table-container {
max-height: 40rem;
overflow: auto;
}
</style>

View File

@@ -1,33 +1,44 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoData component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('network-throughput')"
:value-formatter="customValueFormatter"
/>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("network-throughput") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
</UiCard>
</template>
<script lang="ts" setup>
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { computed, defineAsyncComponent, inject } from "vue";
import { map } from "lodash-es";
import { useI18n } from "vue-i18n";
import { formatSize } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData } from "@/types/chart";
import { map } from "lodash-es";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { hasError, isFetching } = useHostCollection();
const data = computed<LinearChartData>(() => {
const stats = hostLastWeekStats?.stats?.value;
@@ -82,6 +93,25 @@ const data = computed<LinearChartData>(() => {
];
});
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null &&
Object.values(hostStats.pifs["rx"])[0].length +
Object.values(hostStats.pifs["tx"])[0].length ===
data.value[0].data.length + data.value[1].data.length
);
});
});
const isLoading = computed(() => isFetching.value || !isStatFetched.value);
// TODO: improve the way to get the max value of graph
// See: https://github.com/vatesfr/xen-orchestra/pull/6610/files#r1072237279
const customMaxValue = computed(

View File

@@ -1,22 +1,23 @@
<template>
<UiCardTitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
subtitle
/>
<NoDataError v-if="hasError" />
<UsageBar v-else :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { computed, inject, type ComputedRef } from "vue";
import { getAvgCpuUsage } from "@/libs/utils";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { hasError } = useHostCollection();

View File

@@ -1,24 +1,33 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: Display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-cpu-usage')"
:value-formatter="customValueFormatter"
/>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("pool-cpu-usage") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
</UiCard>
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import type { HostStats } from "@/libs/xapi-stats";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
import { computed, defineAsyncComponent, inject } from "vue";
import type { HostStats } from "@/libs/xapi-stats";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { sumBy } from "lodash-es";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useI18n } from "vue-i18n";
const LinearChart = defineAsyncComponent(
@@ -29,8 +38,7 @@ const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { records: hosts } = useHostCollection();
const { records: hosts, isFetching, hasError } = useHostCollection();
const customMaxValue = computed(
() => 100 * sumBy(hosts.value, (host) => +host.cpu_info.cpu_count)
);
@@ -79,6 +87,22 @@ const data = computed<LinearChartData>(() => {
},
];
});
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null &&
Object.values(hostStats.cpus)[0].length === data.value[0].data.length
);
});
});
const isLoading = computed(() => isFetching.value || !isStatFetched.value);
const customValueFormatter: ValueFormatter = (value) => `${value}%`;
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -9,6 +9,7 @@
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
@@ -16,7 +17,7 @@ import { useVmCollection } from "@/stores/xen-api/vm.store";
import { getAvgCpuUsage } from "@/libs/utils";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import { UiCardTitleLevel } from "@/types/enums";
const { hasError } = useVmCollection();

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -13,6 +13,7 @@ import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { type ComputedRef, computed, inject } from "vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";

View File

@@ -1,37 +1,43 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"
:max-value="customMaxValue"
:subtitle="$t('last-week')"
:title="$t('pool-ram-usage')"
:value-formatter="customValueFormatter"
>
<template #summary>
<SizeStatsSummary :size="currentData.size" :usage="currentData.usage" />
</template>
</LinearChart>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("pool-ram-usage") }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
<SizeStatsSummary :size="currentData.size" :usage="currentData.usage" />
</UiCard>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, inject } from "vue";
import { formatSize } from "@/libs/utils";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData } from "@/types/chart";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import { sumBy } from "lodash-es";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { formatSize } from "@/libs/utils";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
import { computed, defineAsyncComponent, inject } from "vue";
import { useI18n } from "vue-i18n";
const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const { runningHosts } = useHostCollection();
const { runningHosts, isFetching, hasError } = useHostCollection();
const { getHostMemory } = useHostMetricsCollection();
const { t } = useI18n();
@@ -92,6 +98,23 @@ const data = computed<LinearChartData>(() => {
];
});
const customValueFormatter: ValueFormatter = (value) =>
String(formatSize(value));
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
if (stats === undefined) {
return false;
}
return stats.every((host) => {
const hostStats = host.stats;
return (
hostStats != null && hostStats.memory.length === data.value[0].data.length
);
});
});
const isLoading = computed(
() => (isFetching.value && !hasError.value) || !isStatFetched.value
);
const customValueFormatter = (value: number) => String(formatSize(value));
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCardTitle
subtitle
:level="UiCardTitleLevel.SubtitleWithUnderline"
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
@@ -9,14 +9,15 @@
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { computed, inject, type ComputedRef } from "vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { UiCardTitleLevel } from "@/types/enums";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
const { hasError } = useVmCollection();

View File

@@ -1,35 +1,40 @@
<template>
<div :class="{ subtitle }" class="ui-section-title">
<component
:is="subtitle ? 'h5' : 'h4'"
v-if="$slots.default || left"
class="left"
>
<div :class="['ui-section-title', tags.left]">
<component :is="tags.left" v-if="$slots.default || left" class="left">
<slot>{{ left }}</slot>
<UiCounter class="count" v-if="count > 0" :value="count" color="info" />
</component>
<component
:is="subtitle ? 'h6' : 'h5'"
v-if="$slots.right || right"
class="right"
>
<component :is="tags.right" v-if="$slots.right || right" class="right">
<slot name="right">{{ right }}</slot>
</component>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import { UiCardTitleLevel } from "@/types/enums";
withDefaults(
const props = withDefaults(
defineProps<{
subtitle?: boolean;
count?: number;
level?: UiCardTitleLevel;
left?: string;
right?: string;
count?: number;
}>(),
{ count: 0 }
{ count: 0, level: UiCardTitleLevel.Title }
);
const tags = computed(() => {
switch (props.level) {
case UiCardTitleLevel.Subtitle:
return { left: "h6", right: "h6" };
case UiCardTitleLevel.SubtitleWithUnderline:
return { left: "h5", right: "h6" };
default:
return { left: "h4", right: "h5" };
}
});
</script>
<style lang="postcss" scoped>
@@ -37,7 +42,6 @@ withDefaults(
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
--section-title-left-size: 2rem;
--section-title-left-color: var(--color-blue-scale-100);
@@ -46,9 +50,17 @@ withDefaults(
--section-title-right-color: var(--color-extra-blue-base);
--section-title-right-weight: 700;
&.subtitle {
border-bottom: 1px solid var(--color-extra-blue-base);
&.h6 {
margin-bottom: 1rem;
--section-title-left-size: 1.5rem;
--section-title-left-color: var(--color-blue-scale-300);
--section-title-left-weight: 400;
}
&.h5 {
margin-top: 2rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-extra-blue-base);
--section-title-left-size: 1.6rem;
--section-title-left-color: var(--color-extra-blue-base);
--section-title-left-weight: 700;

View File

@@ -1,157 +0,0 @@
<template>
<Teleport to="body">
<form
:class="className"
class="ui-modal"
v-bind="$attrs"
@click.self="emit('close')"
>
<div class="container">
<span v-if="onClose" class="close-icon" @click="emit('close')">
<UiIcon :icon="faXmark" />
</span>
<div v-if="icon || $slots.icon" class="modal-icon">
<slot name="icon">
<UiIcon :icon="icon" />
</slot>
</div>
<UiTitle v-if="$slots.title" type="h4">
<slot name="title" />
</UiTitle>
<div v-if="$slots.subtitle" class="subtitle">
<slot name="subtitle" />
</div>
<div v-if="$slots.default" class="content">
<slot />
</div>
<UiButtonGroup :color="color">
<slot name="buttons" />
</UiButtonGroup>
</div>
</form>
</Teleport>
</template>
<script lang="ts" setup>
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useMagicKeys, whenever } from "@vueuse/core";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
color?: "info" | "warning" | "error" | "success";
onClose?: () => void;
}>(),
{ color: "info" }
);
const emit = defineEmits<{
(event: "close"): void;
}>();
const { escape } = useMagicKeys();
whenever(escape, () => emit("close"));
const className = computed(() => {
return [`color-${props.color}`, { "has-icon": props.icon !== undefined }];
});
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background-color: #00000080;
}
.color-success {
--modal-color: var(--color-green-infra-base);
--modal-background-color: var(--background-color-green-infra);
}
.color-info {
--modal-color: var(--color-extra-blue-base);
--modal-background-color: var(--background-color-extra-blue);
}
.color-warning {
--modal-color: var(--color-orange-world-base);
--modal-background-color: var(--background-color-orange-world);
}
.color-error {
--modal-color: var(--color-red-vates-base);
--modal-background-color: var(--background-color-red-vates);
}
.container {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
min-width: 40rem;
padding: 4.2rem;
text-align: center;
border-radius: 1rem;
background-color: var(--modal-background-color);
box-shadow: var(--shadow-400);
}
.close-icon {
font-size: 2rem;
position: absolute;
top: 1.5rem;
right: 2rem;
padding: 0.2rem 0.5rem;
cursor: pointer;
color: var(--modal-color);
}
.container :deep(.accent) {
color: var(--modal-color);
}
.modal-icon {
font-size: 4.8rem;
margin: 2rem 0;
color: var(--modal-color);
}
.ui-title {
margin-top: 4rem;
.has-icon & {
margin-top: 0;
}
}
.subtitle {
font-size: 1.6rem;
font-weight: 400;
color: var(--color-blue-scale-200);
}
.content {
overflow: auto;
font-size: 1.6rem;
max-height: calc(100vh - 40rem);
margin-top: 2rem;
}
.ui-button-group {
margin-top: 4rem;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<UiIcon
:class="textClass"
:icon="faXmark"
class="modal-close-icon"
@click="close"
/>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
const { textClass } = useContext(ColorContext);
const close = inject(IK_MODAL_CLOSE, undefined);
</script>
<style lang="postcss" scoped>
.modal-close-icon {
font-size: 2rem;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<component
:is="tag"
:class="[backgroundClass, { nested: isNested }]"
class="modal-container"
>
<header v-if="$slots.header" class="modal-header">
<slot name="header" />
</header>
<main v-if="$slots.default" class="modal-content">
<slot name="default" />
</main>
<footer v-if="$slots.footer" class="modal-footer">
<slot name="footer" />
</footer>
</component>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_NESTED } from "@/types/injection-keys";
import { inject, provide } from "vue";
const props = withDefaults(
defineProps<{
tag?: string;
color?: Color;
}>(),
{ tag: "div" }
);
defineSlots<{
header: () => any;
default: () => any;
footer: () => any;
}>();
const { backgroundClass } = useContext(ColorContext, () => props.color);
const isNested = inject(IK_MODAL_NESTED, false);
provide(IK_MODAL_NESTED, true);
</script>
<style lang="postcss" scoped>
.modal-container {
display: grid;
grid-template-rows: 1fr auto 1fr;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 20rem);
padding: 2rem;
gap: 1rem;
border-radius: 1rem;
font-size: 1.6rem;
&:not(.nested) {
min-width: 40rem;
box-shadow: var(--shadow-400);
}
&.nested {
margin-top: 2rem;
}
}
.modal-header {
grid-row: 1;
}
.modal-content {
text-align: center;
grid-row: 2;
padding: 2rem;
max-height: 75vh;
overflow: auto;
}
.modal-footer {
grid-row: 3;
align-self: end;
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="ui-modal" @click.self="close">
<slot />
</div>
</Teleport>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { useMagicKeys, useVModel, whenever } from "@vueuse/core/index";
import { provide } from "vue";
const props = defineProps<{
modelValue: boolean;
color?: Color;
}>();
const emit = defineEmits<{
(event: "update:modelValue", value: boolean): void;
}>();
const isOpen = useVModel(props, "modelValue", emit);
const close = () => (isOpen.value = false);
provide(IK_MODAL_CLOSE, close);
useContext(ColorContext, () => props.color);
const { escape } = useMagicKeys();
whenever(escape, () => close());
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background: rgba(26, 27, 56, 0.25);
flex-direction: column;
gap: 2rem;
font-size: 1.6rem;
font-weight: 400;
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<ModalContainer>
<template #header>
<ModalCloseIcon class="close-icon" />
</template>
<template #default>
<slot />
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
defineSlots<{
default: () => void;
}>();
</script>
<style lang="postcss" scoped>
.close-icon {
float: right;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<ModalContainer tag="form">
<template #header>
<div class="close-bar">
<ModalCloseIcon />
</div>
</template>
<template #default>
<UiIcon :class="textClass" :icon="icon" class="main-icon" />
<div v-if="$slots.title || $slots.subtitle" class="titles">
<UiTitle v-if="$slots.title" type="h4">
<slot name="title" />
</UiTitle>
<div v-if="$slots.subtitle" class="subtitle">
<slot name="subtitle" />
</div>
</div>
<div v-if="$slots.default">
<slot name="default" />
</div>
</template>
<template #footer>
<UiButtonGroup>
<slot name="buttons" />
</UiButtonGroup>
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{
icon?: IconDefinition;
}>();
const { textClass } = useContext(ColorContext);
defineSlots<{
title: () => void;
subtitle: () => void;
default: () => void;
buttons: () => void;
}>();
</script>
<style lang="postcss" scoped>
.close-bar {
text-align: right;
}
.main-icon {
font-size: 4.8rem;
margin-bottom: 2rem;
}
.titles {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.subtitle {
font-size: 1.6rem;
font-weight: 400;
color: var(--color-blue-scale-200);
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<ModalContainer tag="form">
<template #header>
<div :class="borderClass" class="title-bar">
<UiIcon :class="textClass" :icon="icon" />
<slot name="title" />
<ModalCloseIcon class="close-icon" />
</div>
</template>
<template #default>
<slot />
</template>
<template #footer>
<UiButtonGroup class="footer-buttons">
<slot name="buttons" />
</UiButtonGroup>
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
disabled?: boolean;
}>(),
{ disabled: undefined }
);
defineSlots<{
title: () => void;
default: () => void;
buttons: () => void;
}>();
const { textClass, borderClass } = useContext(ColorContext);
useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>
.title-bar {
display: flex;
border-bottom-width: 1px;
border-bottom-style: solid;
font-size: 2.4rem;
gap: 1rem;
padding-bottom: 1rem;
font-weight: 500;
align-items: center;
}
.close-icon {
margin-left: auto;
align-self: flex-start;
}
.footer-buttons {
justify-content: flex-end;
}
</style>

View File

@@ -14,7 +14,7 @@
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCopy } from "@fortawesome/free-solid-svg-icons";
@@ -36,7 +36,7 @@ const areAllSelectedVmsHalted = computed(
() =>
selectedVms.value.length > 0 &&
selectedVms.value.every(
(selectedVm) => selectedVm.power_state === POWER_STATE.HALTED
(selectedVm) => selectedVm.power_state === VM_POWER_STATE.HALTED
)
);

View File

@@ -1,48 +1,49 @@
<template>
<MenuItem
v-tooltip="areSomeVmsInExecution && $t('selected-vms-in-execution')"
:disabled="areSomeVmsInExecution"
:disabled="isDisabled"
:icon="faTrashCan"
@click="openDeleteModal"
>
{{ $t("delete") }}
</MenuItem>
<UiModal
v-if="isDeleteModalOpen"
:icon="faSatellite"
@close="closeDeleteModal"
>
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span :class="textClass">
{{ $t("n-vms", { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
</template>
<template #buttons>
<UiButton outlined @click="closeDeleteModal">
{{ $t("go-back") }}
</UiButton>
<UiButton @click="deleteVms">
{{ $t("delete-vms", { n: vmRefs.length }) }}
</UiButton>
</template>
<UiModal v-model="isDeleteModalOpen">
<ConfirmModalLayout :icon="faSatellite">
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span :class="textClass">
{{ $t("n-vms", { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
</template>
<template #buttons>
<UiButton outlined @click="closeDeleteModal">
{{ $t("go-back") }}
</UiButton>
<UiButton @click="deleteVms">
{{ $t("delete-vms", { n: vmRefs.length }) }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import { useContext } from "@/composables/context.composable";
import useModal from "@/composables/modal.composable";
import { ColorContext } from "@/context";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { faSatellite, faTrashCan } from "@fortawesome/free-solid-svg-icons";
@@ -65,7 +66,11 @@ const vms = computed<XenApiVm[]>(() =>
);
const areSomeVmsInExecution = computed(() =>
vms.value.some((vm) => vm.power_state !== POWER_STATE.HALTED)
vms.value.some((vm) => vm.power_state !== VM_POWER_STATE.HALTED)
);
const isDisabled = computed(
() => vms.value.length === 0 || areSomeVmsInExecution.value
);
const deleteVms = async () => {

View File

@@ -0,0 +1,95 @@
<template>
<MenuItem
v-tooltip="
!areAllVmsMigratable && $t('some-selected-vms-can-not-be-migrated')
"
:busy="isMigrating"
:disabled="isParentDisabled || !areAllVmsMigratable"
:icon="faRoute"
@click="openModal"
>
{{ $t("migrate") }}
</MenuItem>
<UiModal v-model="isModalOpen">
<FormModalLayout :disabled="isMigrating" @submit.prevent="handleMigrate">
<template #title>
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</template>
<div>
<FormInputWrapper :label="$t('select-destination-host')" light>
<FormSelect v-model="selectedHost">
<option :value="undefined">
{{ $t("select-destination-host") }}
</option>
<option
v-for="host in availableHosts"
:key="host.$ref"
:value="host"
>
{{ host.name_label }}
</option>
</FormSelect>
</FormInputWrapper>
</div>
<template #buttons>
<UiButton outlined @click="closeModal">
{{ isMigrating ? $t("close") : $t("cancel") }}
</UiButton>
<UiButton :busy="isMigrating" :disabled="!isValid" type="submit">
{{ $t("migrate-n-vms", { n: selectedRefs.length }) }}
</UiButton>
</template>
</FormModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useContext } from "@/composables/context.composable";
import useModal from "@/composables/modal.composable";
import { useVmMigration } from "@/composables/vm-migration.composable";
import { DisabledContext } from "@/context";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { faRoute } from "@fortawesome/free-solid-svg-icons";
const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
}>();
const isParentDisabled = useContext(DisabledContext);
const {
open: openModal,
isOpen: isModalOpen,
close: closeModal,
} = useModal({
onClose: () => (selectedHost.value = undefined),
});
const {
selectedHost,
availableHosts,
isValid,
migrate,
isMigrating,
areAllVmsMigratable,
} = useVmMigration(() => props.selectedRefs);
const handleMigrate = async () => {
try {
await migrate();
closeModal();
} catch (e) {
console.error("Error while migrating", e);
}
};
</script>

View File

@@ -100,7 +100,7 @@ import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { useXenApiStore } from "@/stores/xen-api.store";
import {
faCirclePlay,
@@ -136,16 +136,16 @@ const vmRefsWithPowerState = computed(() =>
const xenApi = useXenApiStore().getXapi();
const areVmsRunning = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.RUNNING)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.RUNNING)
);
const areVmsHalted = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.HALTED)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.HALTED)
);
const areVmsSuspended = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.SUSPENDED)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.SUSPENDED)
);
const areVmsPaused = computed(() =>
vms.value.every((vm) => vm.power_state === POWER_STATE.PAUSED)
vms.value.every((vm) => vm.power_state === VM_POWER_STATE.PAUSED)
);
const areOperationsPending = (operation: VM_OPERATION | VM_OPERATION[]) =>
@@ -179,7 +179,7 @@ const areVmsBusyToForceShutdown = computed(() =>
areOperationsPending(VM_OPERATION.HARD_SHUTDOWN)
);
const getHostState = (host: XenApiHost) =>
isHostRunning(host) ? POWER_STATE.RUNNING : POWER_STATE.HALTED;
isHostRunning(host) ? VM_POWER_STATE.RUNNING : VM_POWER_STATE.HALTED;
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,52 @@
<template>
<MenuItem
:busy="areSomeVmsSnapshoting"
:disabled="isDisabled"
:icon="faCamera"
@click="handleSnapshot"
>
{{ $t("snapshot") }}
</MenuItem>
</template>
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faCamera } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef, isOperationPending } = useVmCollection();
const vms = computed(() =>
props.vmRefs
.map((vmRef) => getByOpaqueRef(vmRef))
.filter((vm): vm is XenApiVm => vm !== undefined)
);
const areSomeVmsSnapshoting = computed(() =>
vms.value.some((vm) => isOperationPending(vm, VM_OPERATION.SNAPSHOT))
);
const isDisabled = computed(
() => vms.value.length === 0 || areSomeVmsSnapshoting.value
);
const handleSnapshot = () => {
const vmRefsToSnapshot = Object.fromEntries(
vms.value.map((vm) => [
vm.$ref,
`${vm.name_label}_${new Date().toISOString()}`,
])
);
return useXenApiStore().getXapi().vm.snapshot(vmRefsToSnapshot);
};
</script>
<style lang="postcss" scoped></style>

View File

@@ -15,16 +15,12 @@
<VmActionPowerStateItems :vm-refs="selectedRefs" />
</template>
</MenuItem>
<MenuItem v-tooltip="$t('coming-soon')" :icon="faRoute">
{{ $t("migrate") }}
</MenuItem>
<VmActionMigrateItem :selected-refs="selectedRefs" />
<VmActionCopyItem :selected-refs="selectedRefs" />
<MenuItem v-tooltip="$t('coming-soon')" :icon="faEdit">
{{ $t("edit-config") }}
</MenuItem>
<MenuItem v-tooltip="$t('coming-soon')" :icon="faCamera">
{{ $t("snapshot") }}
</MenuItem>
<VmActionSnapshotItem :vm-refs="selectedRefs" />
<VmActionExportItem :vm-refs="selectedRefs" />
<VmActionDeleteItem :vm-refs="selectedRefs" />
</AppMenu>
@@ -35,18 +31,18 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
import VmActionDeleteItem from "@/components/vm/VmActionItems/VmActionDeleteItem.vue";
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
import VmActionMigrateItem from "@/components/vm/VmActionItems/VmActionMigrateItem.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useUiStore } from "@/stores/ui.store";
import {
faCamera,
faEdit,
faEllipsis,
faPowerOff,
faRoute,
} from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";

View File

@@ -10,8 +10,7 @@ export const useChartTheme = () => {
const getColors = () => ({
background: style.getPropertyValue("--background-color-primary"),
title: style.getPropertyValue("--color-blue-scale-100"),
subtitle: style.getPropertyValue("--color-blue-scale-300"),
text: style.getPropertyValue("--color-blue-scale-300"),
splitLine: style.getPropertyValue("--color-blue-scale-400"),
primary: style.getPropertyValue("--color-extra-blue-base"),
secondary: style.getPropertyValue("--color-orange-world-base"),
@@ -28,24 +27,10 @@ export const useChartTheme = () => {
backgroundColor: colors.value.background,
textStyle: {},
grid: {
top: 80,
top: 40,
left: 80,
right: 20,
},
title: {
textStyle: {
color: colors.value.title,
fontFamily: "Poppins, sans-serif",
fontWeight: 500,
fontSize: 20,
},
subtextStyle: {
color: colors.value.subtitle,
fontFamily: "Poppins, sans-serif",
fontWeight: 400,
fontSize: 14,
},
},
line: {
itemStyle: {
borderWidth: 2,
@@ -235,7 +220,7 @@ export const useChartTheme = () => {
},
axisLabel: {
show: true,
color: colors.value.subtitle,
color: colors.value.text,
},
splitLine: {
show: true,
@@ -295,7 +280,7 @@ export const useChartTheme = () => {
},
axisLabel: {
show: true,
color: colors.value.subtitle,
color: colors.value.text,
},
splitLine: {
show: true,
@@ -325,7 +310,7 @@ export const useChartTheme = () => {
left: "right",
top: "bottom",
textStyle: {
color: colors.value.subtitle,
color: colors.value.text,
},
},
tooltip: {

View File

@@ -0,0 +1,95 @@
import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useHostStore } from "@/stores/xen-api/host.store";
import type { XenApiPatch } from "@/types/xen-api";
import { type Pausable, useTimeoutPoll, watchArray } from "@vueuse/core";
import { computed, type MaybeRefOrGetter, reactive, toValue } from "vue";
export type XenApiPatchWithHostRefs = XenApiPatch & { $hostRefs: Set<string> };
type HostConfig = {
timeoutPoll: Pausable;
patches: XenApiPatch[];
isLoaded: boolean;
};
export const useHostPatches = (hosts: MaybeRefOrGetter<XenApiHost[]>) => {
const hostStore = useHostStore();
const configByHost = reactive(new Map<string, HostConfig>());
const fetchHostPatches = async (hostRef: XenApiHost["$ref"]) => {
if (!configByHost.has(hostRef)) {
return;
}
const config = configByHost.get(hostRef)!;
config.patches = await hostStore.fetchMissingPatches(hostRef);
config.isLoaded = true;
};
const registerHost = (hostRef: XenApiHost["$ref"]) => {
if (configByHost.has(hostRef)) {
return;
}
const timeoutPoll = useTimeoutPoll(() => fetchHostPatches(hostRef), 10000, {
immediate: true,
});
configByHost.set(hostRef, {
timeoutPoll,
patches: [],
isLoaded: false,
});
};
const unregisterHost = (hostRef: string) => {
configByHost.get(hostRef)?.timeoutPoll.pause();
configByHost.delete(hostRef);
};
watchArray(
() => toValue(hosts).map((host) => host.$ref),
(_n, _p, addedRefs, removedRefs) => {
addedRefs.forEach((ref) => registerHost(ref));
removedRefs?.forEach((ref) => unregisterHost(ref));
},
{ immediate: true }
);
const patches = computed(() => {
const records = new Map<string, XenApiPatchWithHostRefs>();
configByHost.forEach(({ patches }, hostRef) => {
patches.forEach((patch) => {
const record = records.get(patch.$id);
if (record !== undefined) {
return record.$hostRefs.add(hostRef);
}
records.set(patch.$id, {
...patch,
$hostRefs: new Set([hostRef]),
});
});
});
return Array.from(records.values());
});
const count = computed(() => patches.value.length);
const areAllLoaded = computed(() =>
Array.from(configByHost.values()).every((config) => config.isLoaded)
);
const areSomeLoaded = computed(
() =>
areAllLoaded.value ||
Array.from(configByHost.values()).some((config) => config.isLoaded)
);
return { patches, count, areAllLoaded, areSomeLoaded };
};

View File

@@ -1,16 +1,57 @@
# useModal composable
### Usage
#### API
`useModal<T>(options: ModalOptions)`
Type parameter:
- `T`: The type for the modal's payload.
Parameters:
- `options`: An optional object of type `ModalOptions`.
Returns an object with:
- `payload: ReadOnly<Ref<T | undefined>>`: The payload data of the modal. Mainly used if a single modal is used for
multiple items (typically with `v-for`)
- `isOpen: WritableComputedRef<boolean>`: A writable computed indicating if the modal is open or not.
- `open(currentPayload?: T)`: A function to open the modal and optionally set its payload.
- `close(force = false)`: A function to close the modal. If force is set to `true`, the modal will be closed without
calling the `confirmClose` callback.
#### Types
`ModalOptions`
An object type that accepts:
- `confirmClose?: () => boolean`: An optional callback that is called before the modal is closed. If this function
returns `false`, the modal will not be closed.
- `onClose?: () => void`: An optional callback that is called after the modal is closed.
### Example
```vue
<template>
<div v-for="item in items">
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
{{ item.name }}
<button @click="openRemoveModal(item)">Delete</button>
</div>
<UiModal v-if="isRemoveModalOpen">
Are you sure you want to delete {{ removeModalPayload.name }}
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
<UiModal v-model="isRemoveModalOpen">
<ModalContainer>
<template #header>
Are you sure you want to delete {{ removeModalPayload.name }}?
</template>
<template #footer>
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
</template>
</ModalContainer>
</UiModal>
</template>
@@ -22,7 +63,11 @@ const {
isOpen: isRemoveModalOpen,
open: openRemoveModal,
close: closeRemoveModal,
} = useModal();
} = useModal({
confirmClose: () =>
window.confirm("Are you sure you want to close this modal?"),
onClose: () => console.log("Modal closed"),
});
async function handleRemove() {
await removeItem(removeModalPayload.id);

View File

@@ -1,6 +1,11 @@
import { ref } from "vue";
import { computed, readonly, ref } from "vue";
export default function useModal<T>() {
type ModalOptions = {
confirmClose?: () => boolean;
onClose?: () => void;
};
export default function useModal<T>(options: ModalOptions = {}) {
const $payload = ref<T>();
const $isOpen = ref(false);
@@ -8,15 +13,35 @@ export default function useModal<T>() {
$isOpen.value = true;
$payload.value = payload;
};
const close = (force = false) => {
if (!force && options.confirmClose?.() === false) {
return;
}
if (options.onClose) {
options.onClose();
}
const close = () => {
$isOpen.value = false;
$payload.value = undefined;
};
const isOpen = computed({
get() {
return $isOpen.value;
},
set(value) {
if (value) {
open();
} else {
close();
}
},
});
return {
payload: $payload,
isOpen: $isOpen,
payload: readonly($payload),
isOpen,
open,
close,
};

View File

@@ -0,0 +1,82 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import { VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { castArray } from "lodash-es";
import type { MaybeRefOrGetter } from "vue";
import { computed, ref, toValue } from "vue";
export const useVmMigration = (
vmRefs: MaybeRefOrGetter<XenApiVm["$ref"] | XenApiVm["$ref"][]>
) => {
const $isMigrating = ref(false);
const selectedHost = ref<XenApiHost>();
const { getByOpaqueRef: getVm } = useVmCollection();
const { records: hosts } = useHostCollection();
const vms = computed(
() =>
castArray(toValue(vmRefs))
.map((vmRef) => getVm(vmRef))
.filter((vm) => vm !== undefined) as XenApiVm[]
);
const isMigrating = computed(
() =>
$isMigrating.value ||
vms.value.some((vm) =>
Object.values(vm.current_operations).some(
(operation) => operation === VM_OPERATION.POOL_MIGRATE
)
)
);
const availableHosts = computed(() => {
return hosts.value
.filter((host) => vms.value.some((vm) => vm.resident_on !== host.$ref))
.sort(sortRecordsByNameLabel);
});
const areAllVmsMigratable = computed(() =>
vms.value.every((vm) =>
vm.allowed_operations.includes(VM_OPERATION.POOL_MIGRATE)
)
);
const isValid = computed(
() =>
!isMigrating.value &&
vms.value.length > 0 &&
selectedHost.value !== undefined
);
const migrate = async () => {
if (!isValid.value) {
return;
}
try {
$isMigrating.value = true;
const hostRef = selectedHost.value!.$ref;
const xapi = useXenApiStore().getXapi();
await xapi.vm.migrate(
vms.value.map((vm) => vm.$ref),
hostRef
);
} finally {
$isMigrating.value = false;
}
};
return {
isMigrating,
availableHosts,
selectedHost,
areAllVmsMigratable,
isValid,
migrate,
};
};

View File

@@ -12,12 +12,12 @@ export type MixinAbstractConstructor<T = unknown> = abstract new (
export type MixinFunction<
T extends MixinConstructor | MixinAbstractConstructor = MixinConstructor,
R extends T = T & MixinConstructor
R extends T = T & MixinConstructor,
> = (Base: T) => R;
export type MixinReturnValue<
T extends MixinConstructor | MixinAbstractConstructor,
M extends MixinFunction<T, any>[]
M extends MixinFunction<T, any>[],
> = UnionToIntersection<
| T
| {
@@ -27,7 +27,7 @@ export type MixinReturnValue<
export default function mixin<
T extends MixinConstructor | MixinAbstractConstructor,
M extends MixinFunction<T, any>[]
M extends MixinFunction<T, any>[],
>(Base: T, ...mixins: M): MixinReturnValue<T, M> {
return mixins.reduce(
(mix, applyMixin) => applyMixin(mix),

View File

@@ -0,0 +1,493 @@
export enum TASK_ALLOWED_OPERATION {
CANCEL = "cancel",
DESTROY = "destroy",
}
export enum TASK_STATUS_TYPE {
CANCELLED = "cancelled",
CANCELLING = "cancelling",
FAILURE = "failure",
PENDING = "pending",
SUCCESS = "success",
}
export enum EVENT_OPERATION {
ADD = "add",
DEL = "del",
MOD = "mod",
}
export enum POOL_ALLOWED_OPERATION {
APPLY_UPDATES = "apply_updates",
CERT_REFRESH = "cert_refresh",
CLUSTER_CREATE = "cluster_create",
CONFIGURE_REPOSITORIES = "configure_repositories",
COPY_PRIMARY_HOST_CERTS = "copy_primary_host_certs",
DESIGNATE_NEW_MASTER = "designate_new_master",
EXCHANGE_CA_CERTIFICATES_ON_JOIN = "exchange_ca_certificates_on_join",
EXCHANGE_CERTIFICATES_ON_JOIN = "exchange_certificates_on_join",
GET_UPDATES = "get_updates",
HA_DISABLE = "ha_disable",
HA_ENABLE = "ha_enable",
SYNC_UPDATES = "sync_updates",
TLS_VERIFICATION_ENABLE = "tls_verification_enable",
}
export enum TELEMETRY_FREQUENCY {
DAILY = "daily",
MONTHLY = "monthly",
WEEKLY = "weekly",
}
export enum UPDATE_SYNC_FREQUENCY {
DAILY = "daily",
WEEKLY = "weekly",
}
export enum AFTER_APPLY_GUIDANCE {
RESTART_HOST = "restartHost",
RESTART_HVM = "restartHVM",
RESTART_PV = "restartPV",
RESTART_XAPI = "restartXAPI",
}
export enum UPDATE_AFTER_APPLY_GUIDANCE {
RESTART_HOST = "restartHost",
RESTART_HVM = "restartHVM",
RESTART_PV = "restartPV",
RESTART_XAPI = "restartXAPI",
}
export enum LIVEPATCH_STATUS {
OK = "ok",
OK_LIVEPATCH_COMPLETE = "ok_livepatch_complete",
OK_LIVEPATCH_INCOMPLETE = "ok_livepatch_incomplete",
}
export enum VM_POWER_STATE {
HALTED = "Halted",
PAUSED = "Paused",
RUNNING = "Running",
SUSPENDED = "Suspended",
}
export enum UPDATE_GUIDANCE {
REBOOT_HOST = "reboot_host",
REBOOT_HOST_ON_LIVEPATCH_FAILURE = "reboot_host_on_livepatch_failure",
RESTART_DEVICE_MODEL = "restart_device_model",
RESTART_TOOLSTACK = "restart_toolstack",
}
export enum ON_SOFTREBOOT_BEHAVIOR {
DESTROY = "destroy",
PRESERVE = "preserve",
RESTART = "restart",
SOFT_REBOOT = "soft_reboot",
}
export enum ON_NORMAL_EXIT {
DESTROY = "destroy",
RESTART = "restart",
}
export enum VM_OPERATION {
ASSERT_OPERATION_VALID = "assert_operation_valid",
AWAITING_MEMORY_LIVE = "awaiting_memory_live",
CALL_PLUGIN = "call_plugin",
CHANGING_DYNAMIC_RANGE = "changing_dynamic_range",
CHANGING_MEMORY_LIMITS = "changing_memory_limits",
CHANGING_MEMORY_LIVE = "changing_memory_live",
CHANGING_NVRAM = "changing_NVRAM",
CHANGING_SHADOW_MEMORY = "changing_shadow_memory",
CHANGING_SHADOW_MEMORY_LIVE = "changing_shadow_memory_live",
CHANGING_STATIC_RANGE = "changing_static_range",
CHANGING_VCPUS = "changing_VCPUs",
CHANGING_VCPUS_LIVE = "changing_VCPUs_live",
CHECKPOINT = "checkpoint",
CLEAN_REBOOT = "clean_reboot",
CLEAN_SHUTDOWN = "clean_shutdown",
CLONE = "clone",
COPY = "copy",
CREATE_TEMPLATE = "create_template",
CREATE_VTPM = "create_vtpm",
CSVM = "csvm",
DATA_SOURCE_OP = "data_source_op",
DESTROY = "destroy",
EXPORT = "export",
GET_BOOT_RECORD = "get_boot_record",
HARD_REBOOT = "hard_reboot",
HARD_SHUTDOWN = "hard_shutdown",
IMPORT = "import",
MAKE_INTO_TEMPLATE = "make_into_template",
METADATA_EXPORT = "metadata_export",
MIGRATE_SEND = "migrate_send",
PAUSE = "pause",
POOL_MIGRATE = "pool_migrate",
POWER_STATE_RESET = "power_state_reset",
PROVISION = "provision",
QUERY_SERVICES = "query_services",
RESUME = "resume",
RESUME_ON = "resume_on",
REVERT = "revert",
REVERTING = "reverting",
SEND_SYSRQ = "send_sysrq",
SEND_TRIGGER = "send_trigger",
SHUTDOWN = "shutdown",
SNAPSHOT = "snapshot",
SNAPSHOT_WITH_QUIESCE = "snapshot_with_quiesce",
START = "start",
START_ON = "start_on",
SUSPEND = "suspend",
UNPAUSE = "unpause",
UPDATE_ALLOWED_OPERATIONS = "update_allowed_operations",
}
export enum ON_CRASH_BEHAVIOUR {
COREDUMP_AND_DESTROY = "coredump_and_destroy",
COREDUMP_AND_RESTART = "coredump_and_restart",
DESTROY = "destroy",
PRESERVE = "preserve",
RENAME_RESTART = "rename_restart",
RESTART = "restart",
}
export enum DOMAIN_TYPE {
HVM = "hvm",
PV = "pv",
PVH = "pvh",
PV_IN_PVH = "pv_in_pvh",
UNSPECIFIED = "unspecified",
}
export enum TRISTATE_TYPE {
NO = "no",
UNSPECIFIED = "unspecified",
YES = "yes",
}
export enum VMPP_BACKUP_TYPE {
CHECKPOINT = "checkpoint",
SNAPSHOT = "snapshot",
}
export enum VMPP_BACKUP_FREQUENCY {
DAILY = "daily",
HOURLY = "hourly",
WEEKLY = "weekly",
}
export enum VMPP_ARCHIVE_FREQUENCY {
ALWAYS_AFTER_BACKUP = "always_after_backup",
DAILY = "daily",
NEVER = "never",
WEEKLY = "weekly",
}
export enum VMPP_ARCHIVE_TARGET_TYPE {
CIFS = "cifs",
NFS = "nfs",
NONE = "none",
}
export enum VMSS_FREQUENCY {
DAILY = "daily",
HOURLY = "hourly",
WEEKLY = "weekly",
}
export enum VMSS_TYPE {
CHECKPOINT = "checkpoint",
SNAPSHOT = "snapshot",
SNAPSHOT_WITH_QUIESCE = "snapshot_with_quiesce",
}
export enum VM_APPLIANCE_OPERATION {
CLEAN_SHUTDOWN = "clean_shutdown",
HARD_SHUTDOWN = "hard_shutdown",
SHUTDOWN = "shutdown",
START = "start",
}
export enum HOST_ALLOWED_OPERATION {
APPLY_UPDATES = "apply_updates",
EVACUATE = "evacuate",
POWER_ON = "power_on",
PROVISION = "provision",
REBOOT = "reboot",
SHUTDOWN = "shutdown",
VM_MIGRATE = "vm_migrate",
VM_RESUME = "vm_resume",
VM_START = "vm_start",
}
export enum LATEST_SYNCED_UPDATES_APPLIED_STATE {
NO = "no",
UNKNOWN = "unknown",
YES = "yes",
}
export enum HOST_DISPLAY {
DISABLED = "disabled",
DISABLE_ON_REBOOT = "disable_on_reboot",
ENABLED = "enabled",
ENABLE_ON_REBOOT = "enable_on_reboot",
}
export enum HOST_SCHED_GRAN {
CORE = "core",
CPU = "cpu",
SOCKET = "socket",
}
export enum NETWORK_OPERATION {
ATTACHING = "attaching",
}
export enum NETWORK_DEFAULT_LOCKING_MODE {
DISABLED = "disabled",
UNLOCKED = "unlocked",
}
export enum NETWORK_PURPOSE {
INSECURE_NBD = "insecure_nbd",
NBD = "nbd",
}
export enum VIF_OPERATION {
ATTACH = "attach",
PLUG = "plug",
UNPLUG = "unplug",
}
export enum VIF_LOCKING_MODE {
DISABLED = "disabled",
LOCKED = "locked",
NETWORK_DEFAULT = "network_default",
UNLOCKED = "unlocked",
}
export enum VIF_IPV4_CONFIGURATION_MODE {
NONE = "None",
STATIC = "Static",
}
export enum VIF_IPV6_CONFIGURATION_MODE {
NONE = "None",
STATIC = "Static",
}
export enum PIF_IGMP_STATUS {
DISABLED = "disabled",
ENABLED = "enabled",
UNKNOWN = "unknown",
}
export enum IP_CONFIGURATION_MODE {
DHCP = "DHCP",
NONE = "None",
STATIC = "Static",
}
export enum IPV6_CONFIGURATION_MODE {
AUTOCONF = "Autoconf",
DHCP = "DHCP",
NONE = "None",
STATIC = "Static",
}
export enum PRIMARY_ADDRESS_TYPE {
IPV4 = "IPv4",
IPV6 = "IPv6",
}
export enum BOND_MODE {
ACTIVE_BACKUP = "active-backup",
BALANCE_SLB = "balance-slb",
LACP = "lacp",
}
export enum STORAGE_OPERATION {
DESTROY = "destroy",
FORGET = "forget",
PBD_CREATE = "pbd_create",
PBD_DESTROY = "pbd_destroy",
PLUG = "plug",
SCAN = "scan",
UNPLUG = "unplug",
UPDATE = "update",
VDI_CLONE = "vdi_clone",
VDI_CREATE = "vdi_create",
VDI_DATA_DESTROY = "vdi_data_destroy",
VDI_DESTROY = "vdi_destroy",
VDI_DISABLE_CBT = "vdi_disable_cbt",
VDI_ENABLE_CBT = "vdi_enable_cbt",
VDI_INTRODUCE = "vdi_introduce",
VDI_LIST_CHANGED_BLOCKS = "vdi_list_changed_blocks",
VDI_MIRROR = "vdi_mirror",
VDI_RESIZE = "vdi_resize",
VDI_SET_ON_BOOT = "vdi_set_on_boot",
VDI_SNAPSHOT = "vdi_snapshot",
}
export enum SR_HEALTH {
HEALTHY = "healthy",
RECOVERING = "recovering",
}
export enum VDI_OPERATION {
BLOCKED = "blocked",
CLONE = "clone",
COPY = "copy",
DATA_DESTROY = "data_destroy",
DESTROY = "destroy",
DISABLE_CBT = "disable_cbt",
ENABLE_CBT = "enable_cbt",
FORCE_UNLOCK = "force_unlock",
FORGET = "forget",
GENERATE_CONFIG = "generate_config",
LIST_CHANGED_BLOCKS = "list_changed_blocks",
MIRROR = "mirror",
RESIZE = "resize",
RESIZE_ONLINE = "resize_online",
SET_ON_BOOT = "set_on_boot",
SNAPSHOT = "snapshot",
UPDATE = "update",
}
export enum VDI_TYPE {
CBT_METADATA = "cbt_metadata",
CRASHDUMP = "crashdump",
EPHEMERAL = "ephemeral",
HA_STATEFILE = "ha_statefile",
METADATA = "metadata",
PVS_CACHE = "pvs_cache",
REDO_LOG = "redo_log",
RRD = "rrd",
SUSPEND = "suspend",
SYSTEM = "system",
USER = "user",
}
export enum ON_BOOT {
PERSIST = "persist",
RESET = "reset",
}
export enum VBD_OPERATION {
ATTACH = "attach",
EJECT = "eject",
INSERT = "insert",
PAUSE = "pause",
PLUG = "plug",
UNPAUSE = "unpause",
UNPLUG = "unplug",
UNPLUG_FORCE = "unplug_force",
}
export enum VBD_TYPE {
CD = "CD",
DISK = "Disk",
FLOPPY = "Floppy",
}
export enum VBD_MODE {
RO = "RO",
RW = "RW",
}
export enum VTPM_OPERATION {
DESTROY = "destroy",
}
export enum PERSISTENCE_BACKEND {
XAPI = "xapi",
}
export enum CONSOLE_PROTOCOL {
RDP = "rdp",
RFB = "rfb",
VT100 = "vt100",
}
export enum CLS {
CERTIFICATE = "Certificate",
HOST = "Host",
POOL = "Pool",
PVS_PROXY = "PVS_proxy",
SR = "SR",
VDI = "VDI",
VM = "VM",
VMPP = "VMPP",
VMSS = "VMSS",
}
export enum TUNNEL_PROTOCOL {
GRE = "gre",
VXLAN = "vxlan",
}
export enum SRIOV_CONFIGURATION_MODE {
MANUAL = "manual",
MODPROBE = "modprobe",
SYSFS = "sysfs",
UNKNOWN = "unknown",
}
export enum PGPU_DOM0_ACCESS {
DISABLED = "disabled",
DISABLE_ON_REBOOT = "disable_on_reboot",
ENABLED = "enabled",
ENABLE_ON_REBOOT = "enable_on_reboot",
}
export enum ALLOCATION_ALGORITHM {
BREADTH_FIRST = "breadth_first",
DEPTH_FIRST = "depth_first",
}
export enum VGPU_TYPE_IMPLEMENTATION {
GVT_G = "gvt_g",
MXGPU = "mxgpu",
NVIDIA = "nvidia",
NVIDIA_SRIOV = "nvidia_sriov",
PASSTHROUGH = "passthrough",
}
export enum PVS_PROXY_STATUS {
CACHING = "caching",
INCOMPATIBLE_PROTOCOL_VERSION = "incompatible_protocol_version",
INCOMPATIBLE_WRITE_CACHE_MODE = "incompatible_write_cache_mode",
INITIALISED = "initialised",
STOPPED = "stopped",
}
export enum SDN_CONTROLLER_PROTOCOL {
PSSL = "pssl",
SSL = "ssl",
}
export enum VUSB_OPERATION {
ATTACH = "attach",
PLUG = "plug",
UNPLUG = "unplug",
}
export enum CLUSTER_OPERATION {
ADD = "add",
DESTROY = "destroy",
DISABLE = "disable",
ENABLE = "enable",
REMOVE = "remove",
}
export enum CLUSTER_HOST_OPERATION {
DESTROY = "destroy",
DISABLE = "disable",
ENABLE = "enable",
}
export enum CERTIFICATE_TYPE {
CA = "ca",
HOST = "host",
HOST_INTERNAL = "host_internal",
}

View File

@@ -296,7 +296,7 @@ export default class XenApi {
XenApiVm["$ref"],
XenApiVm["power_state"]
>;
type VmRefsToClone = Record<XenApiVm["$ref"], /* Cloned VM name */ string>;
type VmRefsWithNameLabel = Record<XenApiVm["$ref"], string>;
return {
delete: (vmRefs: VmRefs) =>
@@ -351,7 +351,7 @@ export default class XenApi {
)
);
},
clone: (vmRefsToClone: VmRefsToClone) => {
clone: (vmRefsToClone: VmRefsWithNameLabel) => {
const vmRefs = Object.keys(vmRefsToClone) as XenApiVm["$ref"][];
return Promise.all(
@@ -360,6 +360,26 @@ export default class XenApi {
)
);
},
migrate: (vmRefs: VmRefs, destinationHostRef: XenApiHost["$ref"]) => {
return Promise.all(
castArray(vmRefs).map((vmRef) =>
this.call("VM.pool_migrate", [
vmRef,
destinationHostRef,
{ force: "false" },
])
)
);
},
snapshot: (vmRefsToSnapshot: VmRefsWithNameLabel) => {
const vmRefs = Object.keys(vmRefsToSnapshot) as XenApiVm["$ref"][];
return Promise.all(
vmRefs.map((vmRef) =>
this.call("VM.snapshot", [vmRef, vmRefsToSnapshot[vmRef]])
)
);
},
};
}
}

View File

@@ -1,8 +1,46 @@
import type {
XEN_API_OBJECT_TYPES,
POWER_STATE,
ALLOCATION_ALGORITHM,
BOND_MODE,
DOMAIN_TYPE,
IP_CONFIGURATION_MODE,
IPV6_CONFIGURATION_MODE,
NETWORK_DEFAULT_LOCKING_MODE,
NETWORK_OPERATION,
NETWORK_PURPOSE,
ON_BOOT,
ON_CRASH_BEHAVIOUR,
ON_NORMAL_EXIT,
ON_SOFTREBOOT_BEHAVIOR,
PERSISTENCE_BACKEND,
PGPU_DOM0_ACCESS,
PIF_IGMP_STATUS,
PRIMARY_ADDRESS_TYPE,
SRIOV_CONFIGURATION_MODE,
TUNNEL_PROTOCOL,
UPDATE_GUIDANCE,
VBD_MODE,
VBD_OPERATION,
VBD_TYPE,
VDI_OPERATION,
VDI_TYPE,
VGPU_TYPE_IMPLEMENTATION,
VIF_IPV4_CONFIGURATION_MODE,
VIF_IPV6_CONFIGURATION_MODE,
VIF_LOCKING_MODE,
VIF_OPERATION,
VM_APPLIANCE_OPERATION,
VM_OPERATION,
} from "@/libs/xen-api/xen-api.utils";
VM_POWER_STATE,
VMPP_ARCHIVE_FREQUENCY,
VMPP_ARCHIVE_TARGET_TYPE,
VMPP_BACKUP_FREQUENCY,
VMPP_BACKUP_TYPE,
VMSS_FREQUENCY,
VMSS_TYPE,
VTPM_OPERATION,
VUSB_OPERATION,
} from "@/libs/xen-api/xen-api.enums";
import type { XEN_API_OBJECT_TYPES } from "@/libs/xen-api/xen-api.utils";
type TypeMapping = typeof XEN_API_OBJECT_TYPES;
export type ObjectType = keyof TypeMapping;
@@ -81,18 +119,236 @@ export interface XenApiSr extends XenApiRecord<"sr"> {
}
export interface XenApiVm extends XenApiRecord<"vm"> {
current_operations: Record<string, VM_OPERATION>;
guest_metrics: string;
metrics: XenApiVmMetrics["$ref"];
name_label: string;
name_description: string;
power_state: POWER_STATE;
resident_on: XenApiHost["$ref"];
HVM_boot_params: Record<string, string>;
HVM_boot_policy: string;
HVM_shadow_multiplier: number;
NVRAM: Record<string, string>;
PCI_bus: string;
PV_args: string;
PV_bootloader: string;
PV_bootloader_args: string;
PV_kernel: string;
PV_legacy_args: string;
PV_ramdisk: string;
VBDs: XenApiVbd["$ref"][];
VCPUs_at_startup: number;
VCPUs_max: number;
VCPUs_params: Record<string, string>;
VGPUs: XenApiVgpu["$ref"][];
VIFs: XenApiVif["$ref"][];
VTPMs: XenApiVtpm["$ref"][];
VUSBs: XenApiVusb["$ref"][];
actions_after_crash: ON_CRASH_BEHAVIOUR;
actions_after_reboot: ON_NORMAL_EXIT;
actions_after_shutdown: ON_NORMAL_EXIT;
actions_after_softreboot: ON_SOFTREBOOT_BEHAVIOR;
affinity: XenApiHost["$ref"];
allowed_operations: VM_OPERATION[];
appliance: XenApiVmAppliance["$ref"];
attached_PCIs: XenApiPci["$ref"][];
bios_strings: Record<string, string>;
blobs: Record<string, XenApiBlob["$ref"]>;
blocked_operations: Record<VM_OPERATION, string>;
children: XenApiVm["$ref"][];
consoles: XenApiConsole["$ref"][];
is_control_domain: boolean;
crash_dumps: XenApiCrashdump["$ref"][];
current_operations: Record<string, VM_OPERATION>;
domain_type: DOMAIN_TYPE;
domarch: string;
domid: number;
generation_id: string;
guest_metrics: XenApiVmGuestMetrics["$ref"];
ha_always_run: boolean;
ha_restart_priority: string;
hardware_platform_version: number;
has_vendor_device: boolean;
is_a_snapshot: boolean;
is_a_template: boolean;
VCPUs_at_startup: number;
is_control_domain: boolean;
is_default_template: boolean;
is_snapshot_from_vmpp: boolean;
is_vmss_snapshot: boolean;
last_boot_CPU_flags: Record<string, string>;
last_booted_record: string;
memory_dynamic_max: number;
memory_dynamic_min: number;
memory_overhead: number;
memory_static_max: number;
memory_static_min: number;
memory_target: number;
metrics: XenApiVmMetrics["$ref"];
name_description: string;
name_label: string;
order: number;
other_config: Record<string, string>;
parent: XenApiVm["$ref"];
pending_guidances: UPDATE_GUIDANCE[];
platform: Record<string, string>;
power_state: VM_POWER_STATE;
protection_policy: XenApiVmpp["$ref"];
recommendations: string;
reference_label: string;
requires_reboot: boolean;
resident_on: XenApiHost["$ref"];
scheduled_to_be_resident_on: XenApiHost["$ref"];
shutdown_delay: number;
snapshot_info: Record<string, string>;
snapshot_metadata: string;
snapshot_of: XenApiVm["$ref"];
snapshot_schedule: XenApiVmss["$ref"];
snapshot_time: string;
snapshots: XenApiVm["$ref"][];
start_delay: number;
suspend_SR: XenApiSr["$ref"];
suspend_VDI: XenApiVdi["$ref"];
tags: string[];
transportable_snapshot_id: string;
user_version: number;
version: number;
xenstore_data: Record<string, string>;
}
export interface XenApiVtpm extends XenApiRecord<"vtpm"> {
VM: XenApiVm["$ref"];
allowed_operations: VTPM_OPERATION[];
backend: XenApiVm["$ref"];
current_operations: Record<string, VTPM_OPERATION>;
is_protected: boolean;
is_unique: boolean;
persistence_backend: PERSISTENCE_BACKEND;
}
export interface XenApiVusb extends XenApiRecord<"vusb"> {
USB_group: XenApiUsbGroup["$ref"];
VM: XenApiVm["$ref"];
allowed_operations: VUSB_OPERATION[];
current_operations: Record<string, VUSB_OPERATION>;
currently_attached: boolean;
other_config: Record<string, string>;
}
export interface XenApiUsbGroup extends XenApiRecord<"usb_group"> {
PUSBs: XenApiPusb["$ref"][];
VUSBs: XenApiVusb["$ref"][];
name_description: string;
name_label: string;
other_config: Record<string, string>;
}
export interface XenApiPusb extends XenApiRecord<"pusb"> {
USB_group: XenApiUsbGroup["$ref"];
description: string;
host: XenApiHost["$ref"];
other_config: Record<string, string>;
passthrough_enabled: boolean;
path: string;
product_desc: string;
product_id: string;
serial: string;
speed: number;
vendor_desc: string;
vendor_id: string;
version: string;
}
export interface XenApiVgpu extends XenApiRecord<"vgpu"> {
GPU_group: XenApiGpuGroup["$ref"];
PCI: XenApiPci["$ref"];
VM: XenApiVm["$ref"];
compatibility_metadata: Record<string, string>;
currently_attached: boolean;
device: string;
extra_args: string;
other_config: Record<string, string>;
resident_on: XenApiPgpu["$ref"];
scheduled_to_be_resident_on: XenApiPgpu["$ref"];
type: XenApiVgpuType["$ref"];
}
export interface XenApiGpuGroup extends XenApiRecord<"gpu_group"> {
GPU_types: string[];
PGPUs: XenApiPgpu["$ref"][];
VGPUs: XenApiVgpu["$ref"][];
allocation_algorithm: ALLOCATION_ALGORITHM;
enabled_VGPU_types: XenApiVgpuType["$ref"][];
name_description: string;
name_label: string;
other_config: Record<string, string>;
supported_VGPU_types: XenApiVgpuType["$ref"][];
}
export interface XenApiPgpu extends XenApiRecord<"pgpu"> {
GPU_group: XenApiGpuGroup["$ref"];
PCI: XenApiPci["$ref"];
compatibility_metadata: Record<string, string>;
dom0_access: PGPU_DOM0_ACCESS;
enabled_VGPU_types: XenApiVgpuType["$ref"][];
host: XenApiHost["$ref"];
is_system_display_device: boolean;
other_config: Record<string, string>;
resident_VGPUs: XenApiVgpu["$ref"][];
supported_VGPU_max_capacities: Record<XenApiVgpuType["$ref"], number>;
supported_VGPU_types: XenApiVgpuType["$ref"][];
}
export interface XenApiVgpuType extends XenApiRecord<"vgpu_type"> {
VGPUs: XenApiVgpu["$ref"][];
compatible_types_in_vm: XenApiVgpuType["$ref"][];
enabled_on_GPU_groups: XenApiGpuGroup["$ref"][];
enabled_on_PGPUs: XenApiPgpu["$ref"][];
experimental: boolean;
framebuffer_size: number;
identifier: string;
implementation: VGPU_TYPE_IMPLEMENTATION;
max_heads: number;
max_resolution_x: number;
max_resolution_y: number;
model_name: string;
supported_on_GPU_groups: XenApiGpuGroup["$ref"][];
supported_on_PGPUs: XenApiPgpu["$ref"][];
vendor_name: string;
}
export interface XenApiVmAppliance extends XenApiRecord<"vm_appliance"> {
VMs: XenApiVm["$ref"][];
allowed_operations: VM_APPLIANCE_OPERATION[];
current_operations: Record<string, VM_APPLIANCE_OPERATION>;
name_description: string;
name_label: string;
}
export interface XenApiVmpp extends XenApiRecord<"vmpp"> {
VMs: XenApiVm["$ref"][];
alarm_config: Record<string, string>;
archive_frequency: VMPP_ARCHIVE_FREQUENCY;
archive_last_run_time: string;
archive_schedule: Record<string, string>;
archive_target_config: Record<string, string>;
archive_target_type: VMPP_ARCHIVE_TARGET_TYPE;
backup_frequency: VMPP_BACKUP_FREQUENCY;
backup_last_run_time: string;
backup_retention_value: number;
backup_schedule: Record<string, string>;
backup_type: VMPP_BACKUP_TYPE;
is_alarm_enabled: boolean;
is_archive_running: boolean;
is_backup_running: boolean;
is_policy_enabled: boolean;
name_description: string;
name_label: string;
recent_alerts: string[];
}
export interface XenApiVmss extends XenApiRecord<"vmss"> {
VMs: XenApiVm["$ref"][];
enabled: boolean;
frequency: VMSS_FREQUENCY;
last_run_time: string;
name_description: string;
name_label: string;
retained_snapshots: number;
schedule: Record<string, string>;
type: VMSS_TYPE;
}
export interface XenApiConsole extends XenApiRecord<"console"> {
@@ -131,6 +387,238 @@ export interface XenApiMessage<RelationType extends RawObjectType>
timestamp: string;
}
export interface XenApiVbd extends XenApiRecord<"vbd"> {
VDI: XenApiVdi["$ref"];
VM: XenApiVm["$ref"];
allowed_operations: VBD_OPERATION[];
bootable: boolean;
current_operations: Record<string, VBD_OPERATION>;
currently_attached: boolean;
device: string;
empty: boolean;
metrics: XenApiVbdMetrics["$ref"];
mode: VBD_MODE;
other_config: Record<string, string>;
qos_algorithm_params: Record<string, string>;
qos_algorithm_type: string;
qos_supported_algorithms: string[];
runtime_properties: Record<string, string>;
status_code: number;
status_detail: string;
storage_lock: boolean;
type: VBD_TYPE;
unpluggable: boolean;
userdevice: string;
}
export interface XenApiVbdMetrics extends XenApiRecord<"vbd_metrics"> {
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
}
export interface XenApiVdi extends XenApiRecord<"vdi"> {
SR: XenApiSr["$ref"];
VBDs: XenApiVbd["$ref"][];
allow_caching: boolean;
allowed_operations: VDI_OPERATION[];
cbt_enabled: boolean;
crash_dumps: XenApiCrashdump["$ref"][];
current_operations: Record<string, VDI_OPERATION>;
is_a_snapshot: boolean;
is_tools_iso: boolean;
location: string;
managed: boolean;
metadata_latest: boolean;
metadata_of_pool: XenApiPool["$ref"];
missing: boolean;
name_description: string;
name_label: string;
on_boot: ON_BOOT;
other_config: Record<string, string>;
parent: XenApiVdi["$ref"];
physical_utilisation: number;
read_only: boolean;
sharable: boolean;
sm_config: Record<string, string>;
snapshot_of: XenApiVdi["$ref"];
snapshot_time: string;
snapshots: XenApiVdi["$ref"][];
storage_lock: boolean;
tags: string[];
type: VDI_TYPE;
virtual_size: number;
xenstore_data: Record<string, string>;
}
export interface XenApiCrashdump extends XenApiRecord<"crashdump"> {
VDI: XenApiVdi["$ref"];
VM: XenApiVm["$ref"];
other_config: Record<string, string>;
}
export interface XenApiNetwork extends XenApiRecord<"network"> {
MTU: number;
PIFs: XenApiPif["$ref"][];
VIFs: XenApiVif["$ref"][];
allowed_operations: NETWORK_OPERATION[];
assigned_ips: Record<XenApiVif["$ref"], string>;
blobs: Record<string, XenApiBlob["$ref"]>;
bridge: string;
current_operations: Record<string, NETWORK_OPERATION>;
default_locking_mode: NETWORK_DEFAULT_LOCKING_MODE;
managed: boolean;
name_description: string;
name_label: string;
other_config: Record<string, string>;
purpose: NETWORK_PURPOSE[];
tags: string[];
}
export interface XenApiBlob extends XenApiRecord<"blob"> {
last_updated: string;
mime_type: string;
name_description: string;
name_label: string;
public: boolean;
size: number;
}
export interface XenApiVif extends XenApiRecord<"vif"> {
MAC: string;
MAC_autogenerated: boolean;
MTU: number;
VM: XenApiVm["$ref"];
allowed_operations: VIF_OPERATION[];
current_operations: Record<string, VIF_OPERATION>;
currently_attached: boolean;
device: string;
ipv4_addresses: string[];
ipv4_allowed: string[];
ipv4_configuration_mode: VIF_IPV4_CONFIGURATION_MODE;
ipv4_gateway: string;
ipv6_addresses: string[];
ipv6_allowed: string[];
ipv6_configuration_mode: VIF_IPV6_CONFIGURATION_MODE;
ipv6_gateway: string;
locking_mode: VIF_LOCKING_MODE;
metrics: XenApiVifMetrics["$ref"];
network: XenApiNetwork["$ref"];
other_config: Record<string, string>;
qos_algorithm_params: Record<string, string>;
qos_algorithm_type: string;
qos_supported_algorithms: string[];
runtime_properties: Record<string, string>;
status_code: number;
status_detail: string;
}
export interface XenApiVifMetrics extends XenApiRecord<"vif_metrics"> {
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
}
export interface XenApiPif extends XenApiRecord<"pif"> {
DNS: string;
IP: string;
IPv6: string[];
MAC: string;
MTU: number;
PCI: XenApiPci["$ref"];
VLAN: number;
VLAN_master_of: XenApiVlan["$ref"];
VLAN_slave_of: XenApiVlan["$ref"][];
bond_master_of: XenApiBond["$ref"][];
bond_slave_of: XenApiBond["$ref"];
capabilities: string[];
currently_attached: boolean;
device: string;
disallow_unplug: boolean;
gateway: string;
host: XenApiHost["$ref"];
igmp_snooping_status: PIF_IGMP_STATUS;
ip_configuration_mode: IP_CONFIGURATION_MODE;
ipv6_configuration_mode: IPV6_CONFIGURATION_MODE;
ipv6_gateway: string;
managed: boolean;
management: boolean;
metrics: XenApiPifMetrics["$ref"];
netmask: string;
network: XenApiNetwork["$ref"];
other_config: Record<string, string>;
physical: boolean;
primary_address_type: PRIMARY_ADDRESS_TYPE;
properties: Record<string, string>;
sriov_logical_PIF_of: XenApiNetworkSriov["$ref"][];
sriov_physical_PIF_of: XenApiNetworkSriov["$ref"][];
tunnel_access_PIF_of: XenApiTunnel["$ref"][];
tunnel_transport_PIF_of: XenApiTunnel["$ref"][];
}
export interface XenApiNetworkSriov extends XenApiRecord<"network_sriov"> {
configuration_mode: SRIOV_CONFIGURATION_MODE;
logical_PIF: XenApiPif["$ref"];
physical_PIF: XenApiPif["$ref"];
requires_reboot: boolean;
}
export interface XenApiVlan extends XenApiRecord<"vlan"> {
other_config: Record<string, string>;
tag: number;
tagged_PIF: XenApiPif["$ref"];
untagged_PIF: XenApiPif["$ref"];
}
export interface XenApiTunnel extends XenApiRecord<"tunnel"> {
access_PIF: XenApiPif["$ref"];
other_config: Record<string, string>;
protocol: TUNNEL_PROTOCOL;
status: Record<string, string>;
transport_PIF: XenApiPif["$ref"];
}
export interface XenApiPci extends XenApiRecord<"pci"> {
class_name: string;
dependencies: XenApiPci["$ref"][];
device_name: string;
driver_name: string;
host: XenApiHost["$ref"];
other_config: Record<string, string>;
pci_id: string;
subsystem_device_name: string;
subsystem_vendor_name: string;
vendor_name: string;
}
export interface XenApiPifMetrics extends XenApiRecord<"pif_metrics"> {
carrier: boolean;
device_id: string;
device_name: string;
duplex: boolean;
io_read_kbs: number;
io_write_kbs: number;
last_updated: string;
other_config: Record<string, string>;
pci_bus_path: string;
speed: number;
vendor_id: string;
vendor_name: string;
}
export interface XenApiBond extends XenApiRecord<"bond"> {
auto_update_mac: boolean;
links_up: number;
master: XenApiPif["$ref"];
mode: BOND_MODE;
other_config: Record<string, string>;
primary_slave: XenApiPif["$ref"];
properties: Record<string, string>;
slaves: XenApiPif["$ref"][];
}
export type XenApiEvent<
RelationType extends ObjectType,
XRecord extends ObjectTypeToRecord<RelationType>,

View File

@@ -40,6 +40,7 @@ export const XEN_API_OBJECT_TYPES = {
vm: "VM",
vmpp: "VMPP",
vmss: "VMSS",
vm_appliance: "VM_appliance",
vm_guest_metrics: "VM_guest_metrics",
vm_metrics: "VM_metrics",
vusb: "VUSB",
@@ -62,6 +63,7 @@ export const XEN_API_OBJECT_TYPES = {
subject: "subject",
task: "task",
tunnel: "tunnel",
vtpm: "VTPM",
} as const;
export const rawTypeToType = <RawType extends RawObjectType>(
@@ -72,28 +74,6 @@ export const typeToRawType = <Type extends ObjectType>(
type: Type
): TypeToRawType<Type> => XEN_API_OBJECT_TYPES[type];
export enum POWER_STATE {
RUNNING = "Running",
PAUSED = "Paused",
HALTED = "Halted",
SUSPENDED = "Suspended",
}
export enum VM_OPERATION {
START = "start",
START_ON = "start_on",
RESUME = "resume",
UNPAUSE = "unpause",
CLONE = "clone",
SHUTDOWN = "shutdown",
CLEAN_SHUTDOWN = "clean_shutdown",
HARD_SHUTDOWN = "hard_shutdown",
CLEAN_REBOOT = "clean_reboot",
HARD_REBOOT = "hard_reboot",
PAUSE = "pause",
SUSPEND = "suspend",
}
export const buildXoObject = <T extends XenApiRecord<ObjectType>>(
record: RawXenApiRecord<T>,
params: { opaqueRef: T["$ref"] }

View File

@@ -27,10 +27,12 @@
"cancel": "Cancel",
"change-state": "Change state",
"click-to-display-alarms": "Click to display alarms:",
"confirm-delete": "You're about to delete {0}",
"close": "Close",
"coming-soon": "Coming soon!",
"community": "Community",
"community-name": "{name} community",
"confirm-cancel": "Are you sure you want to cancel?",
"confirm-delete": "You're about to delete {0}",
"console": "Console",
"console-unavailable": "Console unavailable",
"copy": "Copy",
@@ -42,9 +44,9 @@
"descending": "descending",
"description": "Description",
"display": "Display",
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"documentation": "Documentation",
"documentation-name": "{name} documentation",
"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",
@@ -76,6 +78,7 @@
"go-back": "Go back",
"here": "Here",
"hosts": "Hosts",
"keep-me-logged": "Keep me logged in",
"language": "Language",
"last-week": "Last week",
"learn-more": "Learn more",
@@ -84,15 +87,18 @@
"log-out": "Log out",
"login": "Login",
"migrate": "Migrate",
"migrate-n-vms": "Migrate 1 VM | Migrate {n} VMs",
"n-hosts-awaiting-patch": "{n} host is awaiting this patch | {n} hosts are awaiting this patch",
"n-missing": "{n} missing",
"n-vms": "1 VM | {n} VMs",
"name": "Name",
"network": "Network",
"network-download": "Download",
"network-throughput": "Network throughput",
"network-upload": "Upload",
"new-features-are-coming": "New features are coming soon!",
"news": "News",
"news-name": "{name} news",
"new-features-are-coming": "New features are coming soon!",
"no-alarm-triggered": "No alarm triggered",
"no-tasks": "No tasks",
"not-found": "Not found",
@@ -104,6 +110,7 @@
"page-not-found": "This page is not to be found…",
"password": "Password",
"password-invalid": "Password invalid",
"patches": "Patches",
"pause": "Pause",
"please-confirm": "Please confirm",
"pool-cpu-usage": "Pool CPU Usage",
@@ -127,11 +134,13 @@
},
"resume": "Resume",
"save": "Save",
"select-destination-host": "Select a destination host",
"selected-vms-in-execution": "Some selected VMs are running",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"shutdown": "Shutdown",
"snapshot": "Snapshot",
"some-selected-vms-can-not-be-migrated": "Some selected VMs can't be migrated",
"sort-by": "Sort by",
"stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage",

View File

@@ -25,12 +25,14 @@
"back-pool-dashboard": "Revenez au tableau de bord de votre pool",
"backup": "Sauvegarde",
"cancel": "Annuler",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"change-state": "Changer l'état",
"click-to-display-alarms": "Cliquer pour afficher les alarmes :",
"close": "Fermer",
"coming-soon": "Bientôt disponible !",
"community": "Communauté",
"community-name": "Communauté {name}",
"confirm-cancel": "Êtes-vous sûr de vouloir annuler ?",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"console": "Console",
"console-unavailable": "Console indisponible",
"copy": "Copier",
@@ -42,9 +44,9 @@
"descending": "descendant",
"description": "Description",
"display": "Affichage",
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"documentation": "Documentation",
"documentation-name": "Documentation {name}",
"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",
@@ -76,6 +78,7 @@
"go-back": "Revenir en arrière",
"here": "Ici",
"hosts": "Hôtes",
"keep-me-logged": "Rester connecté",
"language": "Langue",
"last-week": "Semaine dernière",
"learn-more": "En savoir plus",
@@ -84,15 +87,18 @@
"log-out": "Se déconnecter",
"login": "Connexion",
"migrate": "Migrer",
"migrate-n-vms": "Migrer 1 VM | Migrer {n} VMs",
"n-hosts-awaiting-patch": "{n} hôte attend ce patch | {n} hôtes attendent ce patch",
"n-missing": "{n} manquant | {n} manquants",
"n-vms": "1 VM | {n} VMs",
"name": "Nom",
"network": "Réseau",
"network-download": "Descendant",
"network-throughput": "Débit du réseau",
"network-upload": "Montant",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"news": "Actualités",
"news-name": "Actualités {name}",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"no-alarm-triggered": "Aucune alarme déclenchée",
"no-tasks": "Aucune tâche",
"not-found": "Non trouvé",
@@ -104,6 +110,7 @@
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"patches": "Patches",
"pause": "Pause",
"please-confirm": "Veuillez confirmer",
"pool-cpu-usage": "Utilisation CPU du Pool",
@@ -127,11 +134,13 @@
},
"resume": "Reprendre",
"save": "Enregistrer",
"select-destination-host": "Sélectionnez un hôte de destination",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"shutdown": "Arrêter",
"snapshot": "Instantané",
"some-selected-vms-can-not-be-migrated": "Certaines VMs sélectionnées ne peuvent pas être migrées",
"sort-by": "Trier par",
"stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée",

View File

@@ -2,7 +2,7 @@ import XapiStats from "@/libs/xapi-stats";
import XenApi from "@/libs/xen-api/xen-api";
import { useLocalStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { computed, ref, watchEffect } from "vue";
const HOST_URL = import.meta.env.PROD
? window.origin
@@ -17,16 +17,24 @@ enum STATUS {
export const useXenApiStore = defineStore("xen-api", () => {
const xenApi = new XenApi(HOST_URL);
const xapiStats = new XapiStats(xenApi);
const currentSessionId = useLocalStorage<string | undefined>(
const storedSessionId = useLocalStorage<string | undefined>(
"sessionId",
undefined
);
const currentSessionId = ref(storedSessionId.value);
const rememberMe = useLocalStorage("rememberMe", false);
const status = ref(STATUS.DISCONNECTED);
const isConnected = computed(() => status.value === STATUS.CONNECTED);
const isConnecting = computed(() => status.value === STATUS.CONNECTING);
const getXapi = () => xenApi;
const getXapiStats = () => xapiStats;
watchEffect(() => {
storedSessionId.value = rememberMe.value
? currentSessionId.value
: undefined;
});
const connect = async (username: string, password: string) => {
status.value = STATUS.CONNECTING;
@@ -63,7 +71,7 @@ export const useXenApiStore = defineStore("xen-api", () => {
async function disconnect() {
await xenApi.disconnect();
currentSessionId.value = null;
currentSessionId.value = undefined;
status.value = STATUS.DISCONNECTED;
}

View File

@@ -38,7 +38,7 @@ type StoreToRefs<SS extends Store<any, any, any, any>> = ToRefs<
type Output<
S extends StoreDefinition<any, any, any, any>,
Defer extends boolean
Defer extends boolean,
> = Omit<S, keyof StoreToRefs<S> | IgnoredProperties> &
StoreToRefs<S> &
(Defer extends true
@@ -54,7 +54,7 @@ export const createUseCollection = <
infer A
>
? Store<Id, S, G, A>
: never
: never,
>(
useStore: SD
) => {

View File

@@ -4,6 +4,7 @@ import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { useHostMetricsStore } from "@/stores/xen-api/host-metrics.store";
import type { XenApiPatch } from "@/types/xen-api";
import { defineStore } from "pinia";
import { computed } from "vue";
@@ -42,10 +43,36 @@ export const useHostStore = defineStore("xen-api-host", () => {
});
}) as GetStats<XenApiHost>;
const fetchMissingPatches = async (
hostRef: XenApiHost["$ref"]
): Promise<XenApiPatch[]> => {
const xenApiStore = useXenApiStore();
const rawPatchesAsString = await xenApiStore
.getXapi()
.call<string>("host.call_plugin", [
hostRef,
"updater.py",
"check_update",
{},
]);
const rawPatches = JSON.parse(rawPatchesAsString) as Omit<
XenApiPatch,
"$id"
>[];
return rawPatches.map((rawPatch) => ({
...rawPatch,
$id: `${rawPatch.name}-${rawPatch.version}`,
}));
};
return {
...context,
runningHosts,
getStats,
fetchMissingPatches,
};
});

View File

@@ -3,8 +3,10 @@ import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-s
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api/xen-api.types";
import type { VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import {
type VM_OPERATION,
VM_POWER_STATE,
} from "@/libs/xen-api/xen-api.enums";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { useHostStore } from "@/stores/xen-api/host.store";
@@ -35,7 +37,7 @@ export const useVmStore = defineStore("xen-api-vm", () => {
};
const runningVms = computed(() =>
records.value.filter((vm) => vm.power_state === POWER_STATE.RUNNING)
records.value.filter((vm) => vm.power_state === VM_POWER_STATE.RUNNING)
);
const recordsByHostRef = computed(() => {

View File

@@ -9,6 +9,7 @@
prop('error').type('string').widget(),
prop('help').type('string').widget().preset('256 by default'),
prop('disabled').type('boolean').widget().ctx(),
prop('light').bool().widget(),
slot().help('Contains the input'),
]"
>

View File

@@ -16,8 +16,6 @@ type LinearChartData = {
```vue-template
<LinearChart
title="Chart title"
subtitle="Chart subtitle"
:data="data"
/>
```

View File

@@ -1,8 +1,6 @@
<template>
<ComponentStory
:params="[
prop('title').preset('Chart title').widget(),
prop('subtitle').preset('Here is a subtitle').widget(),
prop('data')
.preset(data)
.required()
@@ -58,8 +56,6 @@ const data: LinearChartData = [
const presets = {
"Network bandwidth": {
props: {
title: "Network bandwidth",
subtitle: "Last week",
"value-formatter": byteFormatter,
"max-value": 500000000,
data: [

View File

@@ -0,0 +1,11 @@
```vue-template
<UiModal v-model="isOpen">
<BasicModalLayout>
Here is a basic modal...
</BasicModalLayout>
</UiModal>
```
```vue-script
const { isOpen } = useModal();
```

View File

@@ -0,0 +1,22 @@
<template>
<ComponentStory
v-slot="{ settings }"
:params="[
slot(),
setting('defaultSlotContent').preset('Modal content').widget(text()),
]"
>
<BasicModalLayout>
{{ settings.defaultSlotContent }}
</BasicModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import { setting, slot } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,21 @@
```vue-template
<UiModal v-model="isOpen">
<ConfirmModalLayout :icon="faShip">
<template #title>Do you confirm?</template>
<template #subtitle>You should be sure about this</template>
<template #buttons>
<UiButton outlined @click="close">I prefer not</UiButton>
<UiButton @click="accept">Yes, I'm sure!</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
```
```vue-script
const { isOpen, close } = useModal();
const accept = async () => {
// do something
close();
}
```

View File

@@ -1,45 +1,32 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
colorProp(),
iconProp(),
event('close').preset(close),
slot('default'),
slot('title'),
slot('subtitle'),
slot('icon'),
slot('default'),
slot('buttons').help('Meant to receive UiButton components'),
setting('title').preset('Modal Title').widget(),
setting('subtitle').preset('Modal Subtitle').widget(),
]"
v-slot="{ properties, settings }"
>
<UiButton type="button" @click="open">Open Modal</UiButton>
<UiModal v-bind="properties" v-if="isOpen">
<ConfirmModalLayout v-bind="properties">
<template #title>{{ settings.title }}</template>
<template #subtitle>{{ settings.subtitle }}</template>
<template #buttons>
<UiButton @click="close">Discard</UiButton>
<UiButton outlined>Discard</UiButton>
<UiButton>Go</UiButton>
</template>
</UiModal>
</ConfirmModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import {
colorProp,
event,
iconProp,
setting,
slot,
} from "@/libs/story/story-param";
const { open, close, isOpen } = useModal();
import { iconProp, setting, slot } from "@/libs/story/story-param";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,25 @@
```vue-template
<UiModal v-model="isOpen">
<FormModalLayout :icon="faShip" @submit.prevent="handleSubmit">
<template #title>Migrate 3 VMs/template>
<template #default>
<!-- Form content goes here... -->
</template>
<template #buttons>
<UiButton outlined @click="close">Cancel</UiButton>
<UiButton type="submit">Migrate 3 VMs</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
```
```vue-script
const { isOpen, close } = useModal();
const handleSubmit = async () => {
// Handling form submission...
close();
}
```

View File

@@ -0,0 +1,54 @@
<template>
<ComponentStory
v-slot="{ properties }"
:params="[iconProp(), slot('title'), slot('default'), slot('buttons')]"
>
<FormModalLayout :icon="faRoute" v-bind="properties">
<template #title>Migrate 3 VMs</template>
<div>
<FormInputWrapper
label="Select a destination host"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
<FormInputWrapper
label="Select a migration network (optional)"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
<FormInputWrapper
help="Individual selection for each VDI is not available on multiple VMs migration."
label="Select a destination SR"
learn-more-url="http://..."
light
>
<FormInput />
</FormInputWrapper>
</div>
<template #buttons>
<UiButton outlined>Cancel</UiButton>
<UiButton>Migrate 3 VMs</UiButton>
</template>
</FormModalLayout>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { iconProp, slot } from "@/libs/story/story-param";
import { faRoute } from "@fortawesome/free-solid-svg-icons";
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,19 @@
A basic modal container containing 3 slots: `header`, `default` and `footer`.
Tag will be `div` by default but can be changed with the `tag` prop.
Color can be changed with the `color` prop.
To keep the content centered vertically, header and footer will always have the same height.
Modal content has an max height + overflow to prevent the modal growing out of the screen.
Modal containers can be nested.
```vue-template
<ModalContainer>
<template #header>Header</template>
<template #default>Content</template>
<template #header>Footer</template>
</ModalContainer>
```

View File

@@ -0,0 +1,52 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
prop('tag').str().default('div').widget(),
colorProp(),
slot('header'),
slot(),
slot('footer'),
setting('headerSlotContent')
.preset('Header')
.widget(text())
.help('Content for default slot'),
setting('defaultSlotContent')
.preset('Content')
.widget(text())
.help('Content for default slot'),
setting('footerSlotContent')
.preset('Footer')
.widget(text())
.help('Content for default slot'),
setting('showNested')
.preset(false)
.widget(boolean())
.help('Show nested modal'),
]"
>
<ModalContainer v-bind="properties">
<template #header>
{{ settings.headerSlotContent }}
</template>
<template #default>
{{ settings.defaultSlotContent }}
<ModalContainer v-if="settings.showNested" color="error">
Nested modal
</ModalContainer>
</template>
<template #footer>
{{ settings.footerSlotContent }}
</template>
</ModalContainer>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
import { colorProp, prop, setting, slot } from "@/libs/story/story-param";
import { boolean, text } from "@/libs/story/story-widget";
</script>

View File

@@ -0,0 +1,21 @@
This component only handle the modal backdrop and content positioning.
You can use any pre-made layouts, create your own or use the `ModalContainer` component.
It is meant to be used with `useModal` composable.
```vue-template
<button @click="open">Delete all items</button>
<UiModal v-model="isOpen">
<ModalContainer...>
<!-- <ConfirmModalLayout ...> (Or you can use a pre-made layout) -->
</UiModal>
```
```vue-script
import { faRemove } from "@fortawesome/free-solid-svg-icons";
import { useModal } from "@composable/modal.composable";
const { open, close, isOpen } = useModal().
```

View File

@@ -0,0 +1,24 @@
<template>
<ComponentStory
:params="[
model()
.required()
.type('boolean')
.help('Whether the modal is opened or not'),
colorProp().ctx(),
slot().help('Place your ModalContainer here'),
]"
>
<button type="button" @click="open">Open modal</button>
<UiModal v-model="isOpen" />
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { colorProp, model, slot } from "@/libs/story/story-param";
const { isOpen, open } = useModal();
</script>

View File

@@ -2,9 +2,9 @@
<ComponentStory
:params="[
prop('state')
.enum(...Object.values(POWER_STATE))
.enum(...Object.values(VM_POWER_STATE))
.required()
.preset(POWER_STATE.RUNNING)
.preset(VM_POWER_STATE.RUNNING)
.widget(),
]"
v-slot="{ properties }"
@@ -17,7 +17,7 @@
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import { prop } from "@/libs/story/story-param";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,19 +0,0 @@
```vue-template
<button @click="open">Delete all items</button>
<UiModal v-if="isOpen" @close="close" :icon="faRemove">
<template #title>You are about to delete 12 items</template>
<template #subtitle>They'll be gone forever</template>
<template #buttons>
<UiButton @click="delete" color="error">Yes, delete</UiButton>
<UiButton @click="close">Cancel</UiButton>
</template>
</UiModal>
```
```vue-script
import { faRemove } from "@fortawesome/free-solid-svg-icons";
import { useModal } from "@composable/modal.composable";
const { open, close, isOpen } = useModal().
```

View File

@@ -0,0 +1,5 @@
export enum UiCardTitleLevel {
Title,
Subtitle,
SubtitleWithUnderline,
}

View File

@@ -47,3 +47,7 @@ export const IK_BUTTON_GROUP_TRANSPARENT = Symbol() as InjectionKey<
export const IK_CARD_GROUP_VERTICAL = Symbol() as InjectionKey<boolean>;
export const IK_INPUT_ID = Symbol() as InjectionKey<ComputedRef<string>>;
export const IK_MODAL_CLOSE = Symbol() as InjectionKey<() => void>;
export const IK_MODAL_NESTED = Symbol() as InjectionKey<boolean>;

View File

@@ -21,3 +21,19 @@ export interface XenApiAlarm<RelationType extends RawObjectType>
triggerLevel: number;
type: XenApiAlarmType;
}
export type XenApiPatch = {
$id: string;
name: string;
description: string;
license: string;
release: string;
size: number;
url: string;
version: string;
changelog: {
date: number;
description: string;
author: string;
};
};

View File

@@ -3,7 +3,7 @@
<UiCardGroup>
<PoolDashboardStatus />
<PoolDashboardAlarms class="alarms" />
<UiCardComingSoon title="Patches" />
<PoolDashboardHostsPatches />
</UiCardGroup>
<UiCardGroup>
<UiCardGroup>
@@ -36,12 +36,12 @@ import PoolDashboardTasks from "@/components/pool/dashboard/PoolDashboardTasks.v
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuProvisioning from "@/components/pool/dashboard/PoolDashboardCpuProvisioning.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardHostsPatches from "@/components/pool/dashboard/PoolDashboardHostsPatches.vue";
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import UiCardComingSoon from "@/components/ui/UiCardComingSoon.vue";
import UiCardGroup from "@/components/ui/UiCardGroup.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";

View File

@@ -12,7 +12,6 @@
:available-filters="filters"
:available-sorts="filters"
:collection="vms"
id-property="$ref"
>
<template #head-row>
<ColumnHeader :icon="faPowerOff" />
@@ -23,7 +22,15 @@
<td>
<PowerStateIcon :state="vm.power_state" />
</td>
<td>{{ vm.name_label }}</td>
<td>
<div class="vm-name">
<UiSpinner
v-if="isMigrating(vm)"
v-tooltip="'This VM is being migrated'"
/>
{{ vm.name_label }}
</div>
</td>
<td>{{ vm.name_description }}</td>
</template>
</CollectionTable>
@@ -36,11 +43,14 @@ import ColumnHeader from "@/components/ColumnHeader.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { POWER_STATE } from "@/libs/xen-api/xen-api.utils";
import { vTooltip } from "@/directives/tooltip.directive";
import { VM_OPERATION, VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { Filters } from "@/types/filter";
import { faPowerOff } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
@@ -52,7 +62,7 @@ const { t } = useI18n();
const titleStore = usePageTitleStore();
titleStore.setTitle(t("vms"));
const { records: vms } = useVmCollection();
const { records: vms, isOperationPending } = useVmCollection();
const { isMobile, isDesktop } = storeToRefs(useUiStore());
const filters: Filters = {
@@ -62,17 +72,26 @@ const filters: Filters = {
label: t("power-state"),
icon: faPowerOff,
type: "enum",
choices: Object.values(POWER_STATE),
choices: Object.values(VM_POWER_STATE),
},
};
const selectedVmsRefs = ref([]);
titleStore.setCount(() => selectedVmsRefs.value.length);
const isMigrating = (vm: XenApiVm) =>
isOperationPending(vm, VM_OPERATION.POOL_MIGRATE);
</script>
<style lang="postcss" scoped>
.pool-vms-view {
overflow: auto;
}
.vm-name {
display: inline-flex;
align-items: center;
gap: 1rem;
}
</style>

View File

@@ -110,19 +110,19 @@ const template = computed(() => {
]"
>
<${componentName} v-bind="properties"${
slotsNames.length > 0
? `>\n ${slotsNames
.map((name) =>
name === "default"
? `{{ settings.${camel(name)}SlotContent }}`
: `<template #${name}>{{ settings.${camel(
name
)}SlotContent }}</template>`
)
.join("\n ")}
slotsNames.length > 0
? `>\n ${slotsNames
.map((name) =>
name === "default"
? `{{ settings.${camel(name)}SlotContent }}`
: `<template #${name}>{{ settings.${camel(
name
)}SlotContent }}</template>`
)
.join("\n ")}
</${componentName}>`
: ` />`
}
: ` />`
}
</ComponentStory>
</template>

View File

@@ -34,7 +34,7 @@ import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useConsoleCollection } from "@/stores/xen-api/console.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.utils";
import { VM_POWER_STATE, VM_OPERATION } from "@/libs/xen-api/xen-api.enums";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
@@ -77,7 +77,7 @@ const hasError = computed(() => hasVmError.value || hasConsoleError.value);
const vm = computed(() => getVmByUuid(route.params.uuid as XenApiVm["uuid"]));
const isVmRunning = computed(
() => vm.value?.power_state === POWER_STATE.RUNNING
() => vm.value?.power_state === VM_POWER_STATE.RUNNING
);
const vmConsole = computed(() => {

View File

@@ -24,6 +24,8 @@ const serializeError = error => ({
})
export default class Tasks extends EventEmitter {
#logsToClearOnSuccess = new Set()
// contains consolidated logs of all live and finished tasks
#store
@@ -36,6 +38,22 @@ export default class Tasks extends EventEmitter {
this.#tasks.delete(id)
},
onTaskUpdate: async taskLog => {
const { id, status } = taskLog
if (status !== 'pending') {
if (this.#logsToClearOnSuccess.has(id)) {
this.#logsToClearOnSuccess.delete(id)
if (status === 'success') {
try {
await this.#store.del(id)
} catch (error) {
warn('failure on deleting task log from store', { error, taskLog })
}
return
}
}
}
// Error objects are not JSON-ifiable by default
const { result } = taskLog
if (result instanceof Error && result.toJSON === undefined) {
@@ -135,10 +153,13 @@ export default class Tasks extends EventEmitter {
*
* @returns {Task}
*/
create({ name, objectId, userId = this.#app.apiContext?.user?.id, type }) {
create(
{ name, objectId, userId = this.#app.apiContext?.user?.id, type, ...props },
{ clearLogOnSuccess = false } = {}
) {
const tasks = this.#tasks
const task = new Task({ properties: { name, objectId, userId, type }, onProgress: this.#onProgress })
const task = new Task({ properties: { ...props, name, objectId, userId, type }, onProgress: this.#onProgress })
// Use a compact, sortable, string representation of the creation date
//
@@ -152,6 +173,9 @@ export default class Tasks extends EventEmitter {
task.id = id
tasks.set(id, task)
if (clearLogOnSuccess) {
this.#logsToClearOnSuccess.add(id)
}
return task
}

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.12.0",
"version": "0.13.0",
"engines": {
"node": ">=15.6"
},

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.33",
"version": "0.26.35",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -32,13 +32,13 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.42.0",
"@xen-orchestra/backups": "^0.43.0",
"@xen-orchestra/fs": "^4.1.0",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.12.0",
"@xen-orchestra/mixins": "^0.13.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^3.1.0",
"@xen-orchestra/xapi": "^3.2.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",

View File

@@ -43,7 +43,7 @@
"pw": "^0.0.4",
"xdg-basedir": "^4.0.0",
"xo-lib": "^0.11.1",
"xo-vmdk-to-vhd": "^2.5.5"
"xo-vmdk-to-vhd": "^2.5.6"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -10,7 +10,7 @@
"@xen-orchestra/log": "^0.6.0",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"vhd-lib": "^4.5.0"
"vhd-lib": "^4.6.1"
},
"engines": {
"node": ">=14"

View File

@@ -1,6 +1,8 @@
import { asyncEach } from '@vates/async-each'
import { asyncMap } from '@xen-orchestra/async-map'
import { decorateClass } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { incorrectState, operationFailed } from 'xo-common/api-errors.js'
import { getCurrentVmUuid } from './_XenStore.mjs'
@@ -31,7 +33,38 @@ class Host {
*
* @param {string} ref - Opaque reference of the host
*/
async smartReboot($defer, ref) {
async smartReboot($defer, ref, bypassBlockedSuspend = false, bypassCurrentVmCheck = false) {
let currentVmRef
try {
currentVmRef = await this.call('VM.get_by_uuid', await getCurrentVmUuid())
} catch (error) {}
const residentVmRefs = await this.getField('host', ref, 'resident_VMs')
const vmsWithSuspendBlocked = await asyncMap(residentVmRefs, ref => this.getRecord('VM', ref)).filter(
vm =>
vm.$ref !== currentVmRef &&
!vm.is_control_domain &&
vm.power_state !== 'Halted' &&
vm.power_state !== 'Suspended' &&
vm.blocked_operations.suspend !== undefined
)
if (!bypassBlockedSuspend && vmsWithSuspendBlocked.length > 0) {
throw incorrectState({ actual: vmsWithSuspendBlocked.map(vm => vm.uuid), expected: [], object: 'suspendBlocked' })
}
if (!bypassCurrentVmCheck && residentVmRefs.includes(currentVmRef)) {
throw operationFailed({
objectId: await this.getField('VM', currentVmRef, 'uuid'),
code: 'xoaOnHost',
})
}
await asyncEach(vmsWithSuspendBlocked, vm => {
$defer(() => vm.update_blocked_operations('suspend', vm.blocked_operations.suspend ?? null))
return vm.update_blocked_operations('suspend', null)
})
const suspendedVms = []
if (await this.getField('host', ref, 'enabled')) {
await this.callAsync('host.disable', ref)
@@ -42,13 +75,8 @@ class Host {
})
}
let currentVmRef
try {
currentVmRef = await this.call('VM.get_by_uuid', await getCurrentVmUuid())
} catch (error) {}
await asyncEach(
await this.getField('host', ref, 'resident_VMs'),
residentVmRefs,
async vmRef => {
if (vmRef === currentVmRef) {
return

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "3.1.0",
"version": "3.2.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -34,7 +34,7 @@
"json-rpc-protocol": "^0.13.2",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.5.0",
"vhd-lib": "^4.6.1",
"xo-common": "^0.8.0"
},
"private": false,

View File

@@ -9,7 +9,7 @@ import peekFooterFromStream from 'vhd-lib/peekFooterFromVhdStream.js'
import AggregateError from './_AggregateError.mjs'
const { warn } = createLogger('xo:xapi:sr')
const { error, warn } = createLogger('xo:xapi:sr')
const OC_MAINTENANCE = 'xo:maintenanceState'
@@ -146,6 +146,22 @@ class Sr {
}
}
async reclaimSpace(srRef) {
const result = await this.call('host.call_plugin', this.pool.master, 'trim', 'do_trim', {
sr_uuid: await this.getField('SR', srRef, 'uuid'),
})
// Error example:
// <?xml version="1.0" ?><trim_response><key_value_pair><key>errcode</key><value>TrimException</value></key_value_pair><key_value_pair><key>errmsg</key><value>blkdiscard: /dev/VG_XenStorage-f5775872-b5e7-98e5-488a-7194efdaf8f6/f5775872-b5e7-98e5-488a-7194efdaf8f6_trim_lv: BLKDISCARD ioctl failed: Operation not supported</value></key_value_pair></trim_response>
const errMatch = result?.match(/<key>errcode<\/key><value>(.*?)<\/value>.*<key>errmsg<\/key><value>(.*?)<\/value>/)
if (errMatch) {
error(result)
const err = new Error(errMatch[2])
err.code = errMatch[1]
throw err
}
}
async importVdi(
$defer,
ref,

View File

@@ -123,7 +123,7 @@ class Vdi {
error.SR = await this.getRecord('SR', vdi.SR)
error.VDI = vdi
error.nbdClient = nbdClient
nbdClient?.disconnect()
await nbdClient?.disconnect()
throw error
}
}
@@ -134,22 +134,23 @@ class Vdi {
if (stream.length === undefined) {
throw new Error('Trying to import a VDI without a length field. Please report this error to Xen Orchestra.')
}
const vdi = await this.getRecord('VDI', ref)
const sr = await this.getRecord('SR', vdi.SR)
try {
await this.putResource(cancelToken, stream, '/import_raw_vdi/', {
query: {
format,
vdi: ref,
},
task: await this.task_create(`Importing content into VDI ${await this.getField('VDI', ref, 'name_label')}`),
task: await this.task_create(`Importing content into VDI ${vdi.name_label} on SR ${sr.name_label}`),
})
} catch (error) {
// augment the error with as much relevant info as possible
const [poolMaster, vdi] = await Promise.all([
this.getRecord('host', this.pool.master),
this.getRecord('VDI', ref),
])
const poolMaster = await this.getRecord('host', this.pool.master)
error.pool_master = poolMaster
error.SR = await this.getRecord('SR', vdi.SR)
error.SR = sr
error.VDI = vdi
throw error
}

View File

@@ -1,8 +1,59 @@
# ChangeLog
## **5.87.0** (2023-09-29)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
- [Patches] Support new XenServer Updates system. See [our documentation](https://xen-orchestra.com/docs/updater.html#xenserver-updates). (PR [#7044](https://github.com/vatesfr/xen-orchestra/pull/7044))
- [Host/Advanced] New button to download system logs [#3968](https://github.com/vatesfr/xen-orchestra/issues/3968) (PR [#7048](https://github.com/vatesfr/xen-orchestra/pull/7048))
- [Home/Hosts, Pools] Display host brand and version (PR [#7027](https://github.com/vatesfr/xen-orchestra/pull/7027))
- [SR] Ability to reclaim space [#1204](https://github.com/vatesfr/xen-orchestra/issues/1204) (PR [#7054](https://github.com/vatesfr/xen-orchestra/pull/7054))
- [XOA] New button to restart XO Server directly from the UI (PR [#7056](https://github.com/vatesfr/xen-orchestra/pull/7056))
- [Host/Advanced] Display system disks health based on the _smartctl_ plugin. [#4458](https://github.com/vatesfr/xen-orchestra/issues/4458) (PR [#7060](https://github.com/vatesfr/xen-orchestra/pull/7060))
- [Authentication] Failed attempts are now logged as XO tasks (PR [#7061](https://github.com/vatesfr/xen-orchestra/pull/7061))
- [Backup] Prevent VMs from being migrated while they are backed up (PR [#7024](https://github.com/vatesfr/xen-orchestra/pull/7024))
- [Backup] Prevent VMs from being backed up while they are migrated (PR [#7024](https://github.com/vatesfr/xen-orchestra/pull/7024))
### Enhancements
- [Netbox] Don't delete VMs that have been created manually in XO-synced cluster [Forum#7639](https://xcp-ng.org/forum/topic/7639) (PR [#7008](https://github.com/vatesfr/xen-orchestra/pull/7008))
- [Kubernetes] _Search domains_ field is now optional [#7028](https://github.com/vatesfr/xen-orchestra/pull/7028)
- [REST API] Hosts' audit and system logs can be downloaded [#3968](https://github.com/vatesfr/xen-orchestra/issues/3968) (PR [#7048](https://github.com/vatesfr/xen-orchestra/pull/7048))
### Bug fixes
- [Backup/Restore] Fix `Cannot read properties of undefined (reading 'id')` error when restoring via an XO Proxy (PR [#7026](https://github.com/vatesfr/xen-orchestra/pull/7026))
- [Google/GitHub Auth] Fix `Internal Server Error` (xo-server: `Cannot read properties of undefined (reading 'id')`) when logging in with Google or GitHub [Forum#7729](https://xcp-ng.org/forum/topic/7729) (PRs [#7031](https://github.com/vatesfr/xen-orchestra/pull/7031) [#7032](https://github.com/vatesfr/xen-orchestra/pull/7032))
- [Jobs] Fix schedules not being displayed on first load [#6968](https://github.com/vatesfr/xen-orchestra/issues/6968) (PR [#7034](https://github.com/vatesfr/xen-orchestra/pull/7034))
- [OVA Export] Fix support of disks with more than 8.2GiB of content (PR [#7047](https://github.com/vatesfr/xen-orchestra/pull/7047))
- [Backup] Fix `VHDFile implementation is not compatible with encrypted remote` when using VHD directory with encryption (PR [#7045](https://github.com/vatesfr/xen-orchestra/pull/7045))
- [Backup/Mirror] Fix `xo:fs:local WARN lock compromised` when mirroring a Backup Repository to a local/NFS/SMB repository ([#7043](https://github.com/vatesfr/xen-orchestra/pull/7043))
- [Ova import] Fix importing VM with collision in disk position (PR [#7051](https://github.com/vatesfr/xen-orchestra/pull/7051)) (issue [7046](https://github.com/vatesfr/xen-orchestra/issues/7046))
- [Backup/Mirror] Fix backup report not being sent (PR [#7049](https://github.com/vatesfr/xen-orchestra/pull/7049))
- [New VM] Only add MBR to cloud-init drive on Windows VMs to avoid booting issues (e.g. with Talos) (PR [#7050](https://github.com/vatesfr/xen-orchestra/pull/7050))
- [VDI Import] Add the SR name to the corresponding XAPI task (PR [#6979](https://github.com/vatesfr/xen-orchestra/pull/6979))
### Released packages
- xo-vmdk-to-vhd 2.5.6
- xo-server-auth-github 0.3.1
- xo-server-auth-google 0.3.1
- xo-server-netbox 1.3.0
- vhd-lib 4.6.1
- @xen-orchestra/xapi 3.2.0
- @xen-orchestra/backups 0.43.0
- @xen-orchestra/backups-cli 1.0.13
- @xen-orchestra/mixins 0.13.0
- @xen-orchestra/proxy 0.26.35
- xo-server 5.124.0
- xo-server-backup-reports 0.17.4
- xo-web 5.126.0
## **5.86.1** (2023-09-07)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Bug fixes
@@ -65,8 +116,6 @@
## **5.85.0** (2023-07-31)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Highlights
- [Import/From VMWare] Support ESXi 6.5+ with snapshot (PR [#6909](https://github.com/vatesfr/xen-orchestra/pull/6909))

View File

@@ -7,18 +7,16 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Netbox] Don't delete VMs that have been created manually in XO-synced cluster [Forum#7639](https://xcp-ng.org/forum/topic/7639) (PR [#7008](https://github.com/vatesfr/xen-orchestra/pull/7008))
- [Host/Advanced] Allow to force _Smart reboot_ if some resident VMs have the suspend operation blocked [Forum#7136](https://xcp-ng.org/forum/topic/7136/suspending-vms-during-host-reboot/23) (PR [#7025](https://github.com/vatesfr/xen-orchestra/pull/7025))
- [Plugin/backup-report] Errors are now listed in XO tasks
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Backup/Restore] Fix `Cannot read properties of undefined (reading 'id')` error when restoring via an XO Proxy (PR [#7026](https://github.com/vatesfr/xen-orchestra/pull/7026))
- [Google/GitHub Auth] Fix `Internal Server Error` (xo-server: `Cannot read properties of undefined (reading 'id')`) when logging in with Google or GitHub [Forum#7729](https://xcp-ng.org/forum/topic/7729) (PRs [#7031](https://github.com/vatesfr/xen-orchestra/pull/7031) [#7032](https://github.com/vatesfr/xen-orchestra/pull/7032))
- [Jobs] Fix schedules not being displayed on first load [#6968](https://github.com/vatesfr/xen-orchestra/issues/6968) (PR [#7034](https://github.com/vatesfr/xen-orchestra/pull/7034))
- [OVA Export] Fix support of disks with more than 8.2GiB of content (PR [#7047](https://github.com/vatesfr/xen-orchestra/pull/7047))
- [Backup] Fix `VHDFile implementation is not compatible with encrypted remote` when using VHD directory with encryption (PR [#7045](https://github.com/vatesfr/xen-orchestra/pull/7045))
- [Backup/Mirror] Fix `xo:fs:local WARN lock compromised` when mirroring a Backup Repository to a local/NFS/SMB repository ([#7043](https://github.com/vatesfr/xen-orchestra/pull/7043))
- [Backup/Mirror] Fix backup report not being sent (PR [#7049](https://github.com/vatesfr/xen-orchestra/pull/7049))
- [New VM] Only add MBR to cloud-init drive on Windows VMs to avoid booting issues (e.g. with Talos) (PR [#7050](https://github.com/vatesfr/xen-orchestra/pull/7050))
- [VDI Import] Add the SR name to the corresponding XAPI task (PR [#6979](https://github.com/vatesfr/xen-orchestra/pull/6979))
### Packages to release
@@ -36,12 +34,10 @@
<!--packages-start-->
- @xen-orchestra/backups patch
- vhd-lib minor
- xo-server patch
- xo-server-auth-github patch
- xo-server-auth-google patch
- xo-server-netbox minor
- xo-web patch
- @xen-orchestra/mixins minor
- @xen-orchestra/xapi minor
- xo-server minor
- xo-server-backup-reports minor
- xo-web minor
<!--packages-end-->

View File

@@ -6,7 +6,6 @@ ISC License may be found here: https://www.isc.org/licenses/
The texts of the two licenses are inserted at the root of this repo.
Below is the list of the various components and their corresponding licenses, AGPL or ISC.
- @xen-orchestra/audit-core - AGPL-3.0-or-later
- @xen-orchestra/babel-config - AGPL-3.0-or-later
- @xen-orchestra/backups - AGPL-3.0-or-later
@@ -51,7 +50,6 @@ Below is the list of the various components and their corresponding licenses, AG
- xo-vmdk-to-vhd - AGPL-3.0-or-later
- xo-web - AGPL-3.0-or-later
- @vates/async-each - ISC
- @vates/cached-dns.lookup - ISC
- @vates/coalesce-calls - ISC

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

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