Compare commits
55 Commits
feat_ssh_t
...
feat_nbd_d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cb986b1a3 | ||
|
|
b86cb12649 | ||
|
|
2af74008b2 | ||
|
|
2e689592f1 | ||
|
|
3f8436b58b | ||
|
|
e3dd59d684 | ||
|
|
549d9b70a9 | ||
|
|
3bf6aae103 | ||
|
|
afb110c473 | ||
|
|
8727c3cf96 | ||
|
|
b13302ddeb | ||
|
|
e89ed06314 | ||
|
|
e3f57998f7 | ||
|
|
8cdb5ee31b | ||
|
|
5b734db656 | ||
|
|
e853f9d04f | ||
|
|
2a5e09719e | ||
|
|
3c0477e0da | ||
|
|
060d1c5297 | ||
|
|
55dd7bfb9c | ||
|
|
b00cf13029 | ||
|
|
73755e4ccf | ||
|
|
a1bd96da6a | ||
|
|
0e934c1413 | ||
|
|
eb69234a8e | ||
|
|
7659d9c0be | ||
|
|
2ba81d55f8 | ||
|
|
2e1abad255 | ||
|
|
c7d5b4b063 | ||
|
|
cc5f4b0996 | ||
|
|
55f627ed83 | ||
|
|
988179a3f0 | ||
|
|
ce617e0732 | ||
|
|
f0f429a473 | ||
|
|
bb6e158301 | ||
|
|
7ff304a042 | ||
|
|
7df1994d7f | ||
|
|
a3a2fda157 | ||
|
|
d8530f9518 | ||
|
|
d3062ac35c | ||
|
|
b11f11f4db | ||
|
|
79d48f3b56 | ||
|
|
869f7ffab0 | ||
|
|
6665d6a8e6 | ||
|
|
8eb0bdbda7 | ||
|
|
710689db0b | ||
|
|
801eea7e75 | ||
|
|
7885e1e6e7 | ||
|
|
d384c746ca | ||
|
|
a30d962b1d | ||
|
|
b6e078716b | ||
|
|
34b69c7ee8 | ||
|
|
70bf8d9620 | ||
|
|
c8bfda9cf5 | ||
|
|
1eb4c20844 |
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
71
@xen-orchestra/lite/src/components/HostPatchesTable.vue
Normal file
71
@xen-orchestra/lite/src/components/HostPatchesTable.vue
Normal 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>
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
57
@xen-orchestra/lite/src/components/ui/modals/UiModal.vue
Normal file
57
@xen-orchestra/lite/src/components/ui/modals/UiModal.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
493
@xen-orchestra/lite/src/libs/xen-api/xen-api.enums.ts
Normal file
493
@xen-orchestra/lite/src/libs/xen-api/xen-api.enums.ts
Normal 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",
|
||||
}
|
||||
@@ -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]])
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
) => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'),
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -16,8 +16,6 @@ type LinearChartData = {
|
||||
|
||||
```vue-template
|
||||
<LinearChart
|
||||
title="Chart title"
|
||||
subtitle="Chart subtitle"
|
||||
:data="data"
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
```vue-template
|
||||
<UiModal v-model="isOpen">
|
||||
<BasicModalLayout>
|
||||
Here is a basic modal...
|
||||
</BasicModalLayout>
|
||||
</UiModal>
|
||||
```
|
||||
|
||||
```vue-script
|
||||
const { isOpen } = useModal();
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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>
|
||||
21
@xen-orchestra/lite/src/stories/modals/ui-modal.story.md
Normal file
21
@xen-orchestra/lite/src/stories/modals/ui-modal.story.md
Normal 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().
|
||||
```
|
||||
24
@xen-orchestra/lite/src/stories/modals/ui-modal.story.vue
Normal file
24
@xen-orchestra/lite/src/stories/modals/ui-modal.story.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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().
|
||||
```
|
||||
5
@xen-orchestra/lite/src/types/enums.ts
Normal file
5
@xen-orchestra/lite/src/types/enums.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum UiCardTitleLevel {
|
||||
Title,
|
||||
Subtitle,
|
||||
SubtitleWithUnderline,
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -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))
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
docs/assets/auth-github-form.png
Normal file
BIN
docs/assets/auth-github-form.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/assets/auth-github-secret.png
Normal file
BIN
docs/assets/auth-github-secret.png
Normal file
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
Reference in New Issue
Block a user