Compare commits
47 Commits
spec/vtpm
...
feat_test_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24f4e15cc6 | ||
|
|
769e27e2cb | ||
|
|
8ec5461338 | ||
|
|
4a2843cb67 | ||
|
|
a0e69a79ab | ||
|
|
3da94f18df | ||
|
|
17cb59b898 | ||
|
|
315e5c9289 | ||
|
|
01ba10fedb | ||
|
|
13e7594560 | ||
|
|
f9ac2ac84d | ||
|
|
09cfac1111 | ||
|
|
008f7a30fd | ||
|
|
ff65dbcba7 | ||
|
|
264a0d1678 | ||
|
|
7dcaf454ed | ||
|
|
17b2756291 | ||
|
|
57e48b5d34 | ||
|
|
57ed984e5a | ||
|
|
100122f388 | ||
|
|
12d4b3396e | ||
|
|
ab35c710cb | ||
|
|
4bd5b38aeb | ||
|
|
836db1b807 | ||
|
|
73d88cc5f1 | ||
|
|
3def66d968 | ||
|
|
3f73138fc3 | ||
|
|
bfe621a21d | ||
|
|
32fa792eeb | ||
|
|
a833050fc2 | ||
|
|
e7e6294bc3 | ||
|
|
7c71884e27 | ||
|
|
3e822044f2 | ||
|
|
d457f5fca4 | ||
|
|
1837e01719 | ||
|
|
f17f5abf0f | ||
|
|
82c229c755 | ||
|
|
c7e3ba3184 | ||
|
|
470c9bb6c8 | ||
|
|
bb3ab20b2a | ||
|
|
90ce1c4d1e | ||
|
|
5c436f3870 | ||
|
|
159339625d | ||
|
|
87e6f7fded | ||
|
|
fd2c7c2fc3 | ||
|
|
7fc76c1df4 | ||
|
|
f2758d036d |
@@ -28,7 +28,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.{spec,test}.{,c,m}js'],
|
||||
files: ['*.{integ,spec,test}.{,c,m}js'],
|
||||
rules: {
|
||||
'n/no-unpublished-require': 'off',
|
||||
'n/no-unpublished-import': 'off',
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^1.3.0"
|
||||
"xen-api": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0",
|
||||
@@ -31,6 +31,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test-integration": "tap *.spec.js"
|
||||
"test-integration": "tap --lines 70 --functions 36 --branches 54 --statements 69 *.integ.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noo
|
||||
assert.notEqual(parent, undefined)
|
||||
|
||||
// inject a (non-enumerable) reference to the parent and the root task
|
||||
Object.defineProperty(taskLog, { $parent: { value: parent }, $root: { value: parent.$root } })
|
||||
Object.defineProperties(taskLog, { $parent: { value: parent }, $root: { value: parent.$root } })
|
||||
;(parent.tasks ?? (parent.tasks = [])).push(taskLog)
|
||||
}
|
||||
} else {
|
||||
|
||||
67
@vates/task/combineEvents.test.js
Normal file
67
@vates/task/combineEvents.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert').strict
|
||||
const { describe, it } = require('test')
|
||||
|
||||
const { makeOnProgress } = require('./combineEvents.js')
|
||||
const { Task } = require('./index.js')
|
||||
|
||||
describe('makeOnProgress()', function () {
|
||||
it('works', async function () {
|
||||
const events = []
|
||||
let log
|
||||
const task = new Task({
|
||||
data: { name: 'task' },
|
||||
onProgress: makeOnProgress({
|
||||
onRootTaskStart(log_) {
|
||||
assert.equal(log, undefined)
|
||||
log = log_
|
||||
events.push('onRootTaskStart')
|
||||
},
|
||||
onRootTaskEnd(log_) {
|
||||
assert.equal(log_, log)
|
||||
events.push('onRootTaskEnd')
|
||||
},
|
||||
|
||||
onTaskUpdate(log_) {
|
||||
assert.equal(log_.$root, log)
|
||||
events.push('onTaskUpdate')
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
assert.equal(events.length, 0)
|
||||
|
||||
await task.run(async () => {
|
||||
assert.equal(events[0], 'onRootTaskStart')
|
||||
assert.equal(events[1], 'onTaskUpdate')
|
||||
assert.equal(log.name, 'task')
|
||||
|
||||
Task.set('progress', 0)
|
||||
assert.equal(events[2], 'onTaskUpdate')
|
||||
assert.equal(log.properties.progress, 0)
|
||||
|
||||
Task.info('foo', {})
|
||||
assert.equal(events[3], 'onTaskUpdate')
|
||||
assert.deepEqual(log.infos, [{ data: {}, message: 'foo' }])
|
||||
|
||||
await Task.run({ data: { name: 'subtask' } }, () => {
|
||||
assert.equal(events[4], 'onTaskUpdate')
|
||||
assert.equal(log.tasks[0].name, 'subtask')
|
||||
|
||||
Task.warning('bar', {})
|
||||
assert.equal(events[5], 'onTaskUpdate')
|
||||
assert.deepEqual(log.tasks[0].warnings, [{ data: {}, message: 'bar' }])
|
||||
})
|
||||
assert.equal(events[6], 'onTaskUpdate')
|
||||
assert.equal(log.tasks[0].status, 'success')
|
||||
|
||||
Task.set('progress', 100)
|
||||
assert.equal(events[7], 'onTaskUpdate')
|
||||
assert.equal(log.properties.progress, 100)
|
||||
})
|
||||
assert.equal(events[8], 'onRootTaskEnd')
|
||||
assert.equal(events[9], 'onTaskUpdate')
|
||||
assert.equal(log.status, 'success')
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.36.0",
|
||||
"@xen-orchestra/backups": "^0.36.1",
|
||||
"@xen-orchestra/fs": "^3.3.4",
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
|
||||
@@ -267,7 +267,7 @@ class VmBackup {
|
||||
await this._callWriters(
|
||||
writer =>
|
||||
writer.transfer({
|
||||
deltaExport: forkDeltaExport(deltaExport),
|
||||
deltaExport: this._writers.size > 1 ? forkDeltaExport(deltaExport) : deltaExport,
|
||||
sizeContainers,
|
||||
timestamp,
|
||||
}),
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.36.0",
|
||||
"version": "0.36.1",
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
"test-integration": "node--test *.integ.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kldzj/stream-throttle": "^1.1.1",
|
||||
|
||||
@@ -50,8 +50,8 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
||||
},
|
||||
})
|
||||
this.transfer = task.wrapFn(this.transfer)
|
||||
this.healthCheck = task.wrapFn(this.healthCheck)
|
||||
this.cleanup = task.wrapFn(this.cleanup, true)
|
||||
this.cleanup = task.wrapFn(this.cleanup)
|
||||
this.healthCheck = task.wrapFn(this.healthCheck, true)
|
||||
|
||||
return task.run(() => this._prepare())
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.5.1",
|
||||
"xen-api": "^1.3.0"
|
||||
"xen-api": "^1.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
<template>
|
||||
<UiModal
|
||||
v-if="isSslModalOpen"
|
||||
:icon="faServer"
|
||||
color="error"
|
||||
@close="clearUnreachableHostsUrls"
|
||||
>
|
||||
<template #title>{{ $t("unreachable-hosts") }}</template>
|
||||
<template #subtitle>{{ $t("following-hosts-unreachable") }}</template>
|
||||
<p>{{ $t("allow-self-signed-ssl") }}</p>
|
||||
<ul>
|
||||
<li v-for="url in unreachableHostsUrls" :key="url.hostname">
|
||||
<a :href="url.href" rel="noopener" target="_blank">{{ url.href }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<template #buttons>
|
||||
<UiButton color="success" @click="reload">
|
||||
{{ $t("unreachable-hosts-reload-page") }}
|
||||
</UiButton>
|
||||
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
<UnreachableHostsModal />
|
||||
<div v-if="!$route.meta.hasStoryNav && !xenApiStore.isConnected">
|
||||
<AppLogin />
|
||||
</div>
|
||||
@@ -41,21 +21,14 @@ import AppHeader from "@/components/AppHeader.vue";
|
||||
import AppLogin from "@/components/AppLogin.vue";
|
||||
import AppNavigation from "@/components/AppNavigation.vue";
|
||||
import AppTooltips from "@/components/AppTooltips.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import UnreachableHostsModal from "@/components/UnreachableHostsModal.vue";
|
||||
import { useChartTheme } from "@/composables/chart-theme.composable";
|
||||
import { useHostStore } from "@/stores/host.store";
|
||||
import { usePoolStore } from "@/stores/pool.store";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
|
||||
import { logicAnd } from "@vueuse/math";
|
||||
import { difference } from "lodash-es";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const unreachableHostsUrls = ref<URL[]>([]);
|
||||
const clearUnreachableHostsUrls = () => (unreachableHostsUrls.value = []);
|
||||
import { computed } from "vue";
|
||||
|
||||
let link = document.querySelector(
|
||||
"link[rel~='icon']"
|
||||
@@ -70,7 +43,6 @@ link.href = favicon;
|
||||
document.title = "XO Lite";
|
||||
|
||||
const xenApiStore = useXenApiStore();
|
||||
const { records: hosts } = useHostStore().subscribe();
|
||||
const { pool } = usePoolStore().subscribe();
|
||||
useChartTheme();
|
||||
const uiStore = useUiStore();
|
||||
@@ -93,17 +65,6 @@ if (import.meta.env.DEV) {
|
||||
);
|
||||
}
|
||||
|
||||
watch(hosts, (hosts, previousHosts) => {
|
||||
difference(hosts, previousHosts).forEach((host) => {
|
||||
const url = new URL("http://localhost");
|
||||
url.protocol = window.location.protocol;
|
||||
url.hostname = host.address;
|
||||
fetch(url, { mode: "no-cors" }).catch(() =>
|
||||
unreachableHostsUrls.value.push(url)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
whenever(
|
||||
() => pool.value?.$ref,
|
||||
async (poolRef) => {
|
||||
@@ -112,9 +73,6 @@ whenever(
|
||||
await xenApi.startWatch();
|
||||
}
|
||||
);
|
||||
|
||||
const isSslModalOpen = computed(() => unreachableHostsUrls.value.length > 0);
|
||||
const reload = () => window.location.reload();
|
||||
</script>
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div v-if="!isDisabled" ref="tooltipElement" class="app-tooltip">
|
||||
<span class="triangle" />
|
||||
<span class="label">{{ content }}</span>
|
||||
<span class="label">{{ options.content }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isEmpty, isFunction, isString } from "lodash-es";
|
||||
import type { TooltipOptions } from "@/stores/tooltip.store";
|
||||
import { isString } from "lodash-es";
|
||||
import place from "placement.js";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import type { TooltipOptions } from "@/stores/tooltip.store";
|
||||
|
||||
const props = defineProps<{
|
||||
target: HTMLElement;
|
||||
@@ -18,29 +18,13 @@ const props = defineProps<{
|
||||
|
||||
const tooltipElement = ref<HTMLElement>();
|
||||
|
||||
const content = computed(() =>
|
||||
isString(props.options) ? props.options : props.options.content
|
||||
const isDisabled = computed(() =>
|
||||
isString(props.options.content)
|
||||
? props.options.content.trim() === ""
|
||||
: props.options.content === false
|
||||
);
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
if (isEmpty(content.value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isString(props.options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFunction(props.options.disabled)) {
|
||||
return props.options.disabled(props.target);
|
||||
}
|
||||
|
||||
return props.options.disabled ?? false;
|
||||
});
|
||||
|
||||
const placement = computed(() =>
|
||||
isString(props.options) ? "top" : props.options.placement ?? "top"
|
||||
);
|
||||
const placement = computed(() => props.options.placement ?? "top");
|
||||
|
||||
watchEffect(() => {
|
||||
if (tooltipElement.value) {
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
</UiActionButton>
|
||||
</UiFilterGroup>
|
||||
|
||||
<UiModal v-if="isOpen" :icon="faFilter" @submit.prevent="handleSubmit">
|
||||
<UiModal
|
||||
v-if="isOpen"
|
||||
:icon="faFilter"
|
||||
@submit.prevent="handleSubmit"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<div class="rows">
|
||||
<CollectionFilterRow
|
||||
v-for="(newFilter, index) in newFilters"
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
</UiActionButton>
|
||||
</UiFilterGroup>
|
||||
|
||||
<UiModal v-if="isOpen" :icon="faSort" @submit.prevent="handleSubmit">
|
||||
<UiModal
|
||||
v-if="isOpen"
|
||||
:icon="faSort"
|
||||
@submit.prevent="handleSubmit"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<div class="form-widgets">
|
||||
<FormWidget :label="$t('sort-by')">
|
||||
<select v-model="newSortProperty">
|
||||
|
||||
59
@xen-orchestra/lite/src/components/UnreachableHostsModal.vue
Normal file
59
@xen-orchestra/lite/src/components/UnreachableHostsModal.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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";
|
||||
import { useHostStore } from "@/stores/host.store";
|
||||
|
||||
const { records: hosts } = useHostStore().subscribe();
|
||||
const unreachableHostsUrls = ref<Set<string>>(new Set());
|
||||
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
|
||||
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);
|
||||
const reload = () => window.location.reload();
|
||||
|
||||
watch(hosts, (nextHosts, previousHosts) => {
|
||||
difference(nextHosts, previousHosts).forEach((host) => {
|
||||
const url = new URL("http://localhost");
|
||||
url.protocol = window.location.protocol;
|
||||
url.hostname = host.address;
|
||||
fetch(url, { mode: "no-cors" }).catch(() =>
|
||||
unreachableHostsUrls.value.add(url.toString())
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.description p {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,5 @@
|
||||
<template>
|
||||
<li
|
||||
v-if="host !== undefined"
|
||||
v-tooltip="{
|
||||
content: host.name_label,
|
||||
disabled: isTooltipDisabled,
|
||||
}"
|
||||
class="infra-host-item"
|
||||
>
|
||||
<li v-if="host !== undefined" class="infra-host-item">
|
||||
<InfraItemLabel
|
||||
:active="isCurrentHost"
|
||||
:icon="faServer"
|
||||
@@ -36,7 +29,6 @@ import InfraAction from "@/components/infra/InfraAction.vue";
|
||||
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
||||
import InfraVmList from "@/components/infra/InfraVmList.vue";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import { hasEllipsis } from "@/libs/utils";
|
||||
import { useHostStore } from "@/stores/host.store";
|
||||
import { usePoolStore } from "@/stores/pool.store";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
@@ -66,9 +58,6 @@ const isCurrentHost = computed(
|
||||
() => props.hostOpaqueRef === uiStore.currentHostOpaqueRef
|
||||
);
|
||||
const [isExpanded, toggle] = useToggle(true);
|
||||
|
||||
const isTooltipDisabled = (target: HTMLElement) =>
|
||||
!hasEllipsis(target.querySelector(".text"));
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
class="infra-item-label"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<a :href="href" class="link" @click="navigate">
|
||||
<a :href="href" class="link" @click="navigate" v-tooltip="hasTooltip">
|
||||
<UiIcon :icon="icon" class="icon" />
|
||||
<div class="text">
|
||||
<div ref="textElement" class="text">
|
||||
<slot />
|
||||
</div>
|
||||
</a>
|
||||
@@ -22,7 +22,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import { hasEllipsis } from "@/libs/utils";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { computed, ref } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
defineProps<{
|
||||
@@ -30,6 +33,9 @@ defineProps<{
|
||||
route: RouteLocationRaw;
|
||||
active?: boolean;
|
||||
}>();
|
||||
|
||||
const textElement = ref<HTMLElement>();
|
||||
const hasTooltip = computed(() => hasEllipsis(textElement.value));
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<template>
|
||||
<li
|
||||
v-if="vm !== undefined"
|
||||
ref="rootElement"
|
||||
v-tooltip="{
|
||||
content: vm.name_label,
|
||||
disabled: isTooltipDisabled,
|
||||
}"
|
||||
class="infra-vm-item"
|
||||
>
|
||||
<li v-if="vm !== undefined" ref="rootElement" class="infra-vm-item">
|
||||
<InfraItemLabel
|
||||
v-if="isVisible"
|
||||
:icon="faDisplay"
|
||||
@@ -27,8 +19,6 @@
|
||||
import InfraAction from "@/components/infra/InfraAction.vue";
|
||||
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
||||
import PowerStateIcon from "@/components/PowerStateIcon.vue";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import { hasEllipsis } from "@/libs/utils";
|
||||
import { useVmStore } from "@/stores/vm.store";
|
||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useIntersectionObserver } from "@vueuse/core";
|
||||
@@ -49,9 +39,6 @@ const { stop } = useIntersectionObserver(rootElement, ([entry]) => {
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
const isTooltipDisabled = (target: HTMLElement) =>
|
||||
!hasEllipsis(target.querySelector(".text"));
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
@@ -16,6 +16,7 @@ defineProps<{
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-badge {
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<div class="legend">
|
||||
<span class="circle" />
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<template v-if="$slots.label || label">
|
||||
<span class="circle" />
|
||||
<div class="label-container">
|
||||
<div ref="labelElement" v-tooltip="isTooltipEnabled" class="label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<UiBadge class="badge">
|
||||
<slot name="value">{{ value }}</slot>
|
||||
</UiBadge>
|
||||
@@ -10,14 +16,23 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import { hasEllipsis } from "@/libs/utils";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
defineProps<{
|
||||
label?: string;
|
||||
value?: string;
|
||||
}>();
|
||||
|
||||
const labelElement = ref<HTMLElement>();
|
||||
|
||||
const isTooltipEnabled = computed(() =>
|
||||
hasEllipsis(labelElement.value, { vertical: true })
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style lang="postcss" scoped>
|
||||
.badge {
|
||||
font-size: 0.9em;
|
||||
font-weight: 700;
|
||||
@@ -25,8 +40,8 @@ defineProps<{
|
||||
|
||||
.circle {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--progress-bar-color);
|
||||
}
|
||||
@@ -38,4 +53,14 @@ defineProps<{
|
||||
gap: 0.5rem;
|
||||
margin: 1.6em 0;
|
||||
}
|
||||
|
||||
.label-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
|
||||
37
@xen-orchestra/lite/src/components/vm/VmTabBar.vue
Normal file
37
@xen-orchestra/lite/src/components/vm/VmTabBar.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<UiTabBar>
|
||||
<RouterTab :to="{ name: 'vm.dashboard', params: { uuid } }">
|
||||
{{ $t("dashboard") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'vm.console', params: { uuid } }">
|
||||
{{ $t("console") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'vm.alarms', params: { uuid } }">
|
||||
{{ $t("alarms") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'vm.stats', params: { uuid } }">
|
||||
{{ $t("stats") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'vm.system', params: { uuid } }">
|
||||
{{ $t("system") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'vm.network', params: { uuid } }">
|
||||
{{ $t("network") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'vm.storage', params: { uuid } }">
|
||||
{{ $t("storage") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'vm.tasks', params: { uuid } }">
|
||||
{{ $t("tasks") }}
|
||||
</RouterTab>
|
||||
</UiTabBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RouterTab from "@/components/RouterTab.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
|
||||
defineProps<{
|
||||
uuid: string;
|
||||
}>();
|
||||
</script>
|
||||
@@ -1,36 +1,71 @@
|
||||
# Tooltip Directive
|
||||
|
||||
By default, tooltip will appear centered above the target element.
|
||||
By default, the tooltip will appear centered above the target element.
|
||||
|
||||
## Directive argument
|
||||
|
||||
The directive argument can be either:
|
||||
|
||||
- The tooltip content
|
||||
- An object containing the tooltip content and/or placement: `{ content: "...", placement: "..." }` (both optional)
|
||||
|
||||
## Tooltip content
|
||||
|
||||
The tooltip content can be either:
|
||||
|
||||
- `false` or an empty-string to disable the tooltip
|
||||
- `true` or `undefined` to enable the tooltip and extract its content from the element's innerText.
|
||||
- Non-empty string to enable the tooltip and use the string as content.
|
||||
|
||||
## Tooltip placement
|
||||
|
||||
Tooltip can be placed on the following positions:
|
||||
|
||||
- `top`
|
||||
- `top-start`
|
||||
- `top-end`
|
||||
- `bottom`
|
||||
- `bottom-start`
|
||||
- `bottom-end`
|
||||
- `left`
|
||||
- `left-start`
|
||||
- `left-end`
|
||||
- `right`
|
||||
- `right-start`
|
||||
- `right-end`
|
||||
|
||||
## Usage
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Static -->
|
||||
<!-- Boolean / Undefined -->
|
||||
<span v-tooltip="true"
|
||||
>This content will be ellipsized by CSS but displayed entirely in the
|
||||
tooltip</span
|
||||
>
|
||||
<span v-tooltip
|
||||
>This content will be ellipsized by CSS but displayed entirely in the
|
||||
tooltip</span
|
||||
>
|
||||
|
||||
<!-- String -->
|
||||
<span v-tooltip="'Tooltip content'">Item</span>
|
||||
|
||||
<!-- Dynamic -->
|
||||
<span v-tooltip="myTooltipContent">Item</span>
|
||||
|
||||
<!-- Placement -->
|
||||
<!-- Object -->
|
||||
<span v-tooltip="{ content: 'Foobar', placement: 'left-end' }">Item</span>
|
||||
|
||||
<!-- Disabling (variable) -->
|
||||
<span v-tooltip="{ content: 'Foobar', disabled: isDisabled }">Item</span>
|
||||
<!-- Dynamic -->
|
||||
<span v-tooltip="myTooltip">Item</span>
|
||||
|
||||
<!-- Disabling (function) -->
|
||||
<span v-tooltip="{ content: 'Foobar', disabled: isDisabledFn }">Item</span>
|
||||
<!-- Conditional -->
|
||||
<span v-tooltip="isTooltipEnabled && 'Foobar'">Item</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
|
||||
const myTooltipContent = ref("Content");
|
||||
const isDisabled = ref(true);
|
||||
|
||||
const isDisabledFn = (target: Element) => {
|
||||
// return boolean;
|
||||
};
|
||||
const myTooltip = ref("Content"); // or ref({ content: "Content", placement: "left-end" })
|
||||
const isTooltipEnabled = ref(true);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
import type { Directive } from "vue";
|
||||
import type { TooltipEvents, TooltipOptions } from "@/stores/tooltip.store";
|
||||
import { useTooltipStore } from "@/stores/tooltip.store";
|
||||
import { isObject } from "lodash-es";
|
||||
import type { Options } from "placement.js";
|
||||
import type { Directive } from "vue";
|
||||
|
||||
export const vTooltip: Directive<HTMLElement, TooltipOptions> = {
|
||||
type TooltipDirectiveContent = undefined | boolean | string;
|
||||
|
||||
type TooltipDirectiveOptions =
|
||||
| TooltipDirectiveContent
|
||||
| {
|
||||
content?: TooltipDirectiveContent;
|
||||
placement?: Options["placement"];
|
||||
};
|
||||
|
||||
const parseOptions = (
|
||||
options: TooltipDirectiveOptions,
|
||||
target: HTMLElement
|
||||
): TooltipOptions => {
|
||||
const { placement, content } = isObject(options)
|
||||
? options
|
||||
: { placement: undefined, content: options };
|
||||
|
||||
return {
|
||||
placement,
|
||||
content:
|
||||
content === true || content === undefined
|
||||
? target.innerText.trim()
|
||||
: content,
|
||||
};
|
||||
};
|
||||
|
||||
export const vTooltip: Directive<HTMLElement, TooltipDirectiveOptions> = {
|
||||
mounted(target, binding) {
|
||||
const store = useTooltipStore();
|
||||
|
||||
@@ -10,11 +38,11 @@ export const vTooltip: Directive<HTMLElement, TooltipOptions> = {
|
||||
? { on: "focusin", off: "focusout" }
|
||||
: { on: "mouseenter", off: "mouseleave" };
|
||||
|
||||
store.register(target, binding.value, events);
|
||||
store.register(target, parseOptions(binding.value, target), events);
|
||||
},
|
||||
updated(target, binding) {
|
||||
const store = useTooltipStore();
|
||||
store.updateOptions(target, binding.value);
|
||||
store.updateOptions(target, parseOptions(binding.value, target));
|
||||
},
|
||||
beforeUnmount(target) {
|
||||
const store = useTooltipStore();
|
||||
|
||||
@@ -71,8 +71,20 @@ export function parseDateTime(dateTime: string) {
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
export const hasEllipsis = (target: Element | undefined | null) =>
|
||||
target != undefined && target.clientWidth < target.scrollWidth;
|
||||
export const hasEllipsis = (
|
||||
target: Element | undefined | null,
|
||||
{ vertical = false }: { vertical?: boolean } = {}
|
||||
) => {
|
||||
if (target == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vertical) {
|
||||
return target.clientHeight < target.scrollHeight;
|
||||
}
|
||||
|
||||
return target.clientWidth < target.scrollWidth;
|
||||
};
|
||||
|
||||
export function percent(currentValue: number, maxValue: number, precision = 2) {
|
||||
return round((currentValue / maxValue) * 100, precision);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"coming-soon": "Coming soon!",
|
||||
"community": "Community",
|
||||
"community-name": "{name} community",
|
||||
"console": "Console",
|
||||
"copy": "Copy",
|
||||
"cpu-provisioning": "CPU provisioning",
|
||||
"cpu-usage": "CPU usage",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"coming-soon": "Bientôt disponible !",
|
||||
"community": "Communauté",
|
||||
"community-name": "Communauté {name}",
|
||||
"console": "Console",
|
||||
"copy": "Copier",
|
||||
"cpu-provisioning": "Provisionnement CPU",
|
||||
"cpu-usage": "Utilisation CPU",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import pool from "@/router/pool";
|
||||
import vm from "@/router/vm";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import HostDashboardView from "@/views/host/HostDashboardView.vue";
|
||||
import HostRootView from "@/views/host/HostRootView.vue";
|
||||
import PageNotFoundView from "@/views/PageNotFoundView.vue";
|
||||
import SettingsView from "@/views/settings/SettingsView.vue";
|
||||
import StoryView from "@/views/StoryView.vue";
|
||||
import VmConsoleView from "@/views/vm/VmConsoleView.vue";
|
||||
import VmRootView from "@/views/vm/VmRootView.vue";
|
||||
import storiesRoutes from "virtual:stories";
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
|
||||
@@ -31,6 +30,7 @@ const router = createRouter({
|
||||
component: SettingsView,
|
||||
},
|
||||
pool,
|
||||
vm,
|
||||
{
|
||||
path: "/host/:uuid",
|
||||
component: HostRootView,
|
||||
@@ -42,17 +42,6 @@ const router = createRouter({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/vm/:uuid",
|
||||
component: VmRootView,
|
||||
children: [
|
||||
{
|
||||
path: "console",
|
||||
name: "vm.console",
|
||||
component: VmConsoleView,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "notFound",
|
||||
|
||||
47
@xen-orchestra/lite/src/router/vm.ts
Normal file
47
@xen-orchestra/lite/src/router/vm.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export default {
|
||||
path: "/vm/:uuid",
|
||||
component: () => import("@/views/vm/VmRootView.vue"),
|
||||
redirect: { name: "vm.console" },
|
||||
children: [
|
||||
{
|
||||
path: "dashboard",
|
||||
name: "vm.dashboard",
|
||||
component: () => import("@/views/vm/VmDashboardView.vue"),
|
||||
},
|
||||
{
|
||||
path: "console",
|
||||
name: "vm.console",
|
||||
component: () => import("@/views/vm/VmConsoleView.vue"),
|
||||
},
|
||||
{
|
||||
path: "alarms",
|
||||
name: "vm.alarms",
|
||||
component: () => import("@/views/vm/VmAlarmsView.vue"),
|
||||
},
|
||||
{
|
||||
path: "stats",
|
||||
name: "vm.stats",
|
||||
component: () => import("@/views/vm/VmStatsView.vue"),
|
||||
},
|
||||
{
|
||||
path: "system",
|
||||
name: "vm.system",
|
||||
component: () => import("@/views/vm/VmSystemView.vue"),
|
||||
},
|
||||
{
|
||||
path: "network",
|
||||
name: "vm.network",
|
||||
component: () => import("@/views/vm/VmNetworkView.vue"),
|
||||
},
|
||||
{
|
||||
path: "storage",
|
||||
name: "vm.storage",
|
||||
component: () => import("@/views/vm/VmStorageView.vue"),
|
||||
},
|
||||
{
|
||||
path: "tasks",
|
||||
name: "vm.tasks",
|
||||
component: () => import("@/views/vm/VmTasksView.vue"),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -4,13 +4,10 @@ import type { Options } from "placement.js";
|
||||
import { type EffectScope, computed, effectScope, ref } from "vue";
|
||||
import { type WindowEventName, useEventListener } from "@vueuse/core";
|
||||
|
||||
export type TooltipOptions =
|
||||
| string
|
||||
| {
|
||||
content: string;
|
||||
placement?: Options["placement"];
|
||||
disabled?: boolean | ((target: HTMLElement) => boolean);
|
||||
};
|
||||
export type TooltipOptions = {
|
||||
content: string | false;
|
||||
placement: Options["placement"];
|
||||
};
|
||||
|
||||
export type TooltipEvents = { on: WindowEventName; off: WindowEventName };
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<PoolDashboardRamUsage />
|
||||
</div>
|
||||
<div class="item">
|
||||
<PoolDashboardCpuProvisionning />
|
||||
<PoolDashboardCpuProvisioning />
|
||||
</div>
|
||||
<div class="item">
|
||||
<PoolDashboardNetworkChart />
|
||||
@@ -38,7 +38,7 @@ import { provide, watch } from "vue";
|
||||
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
|
||||
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
|
||||
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
|
||||
import PoolDashboardCpuProvisionning from "@/components/pool/dashboard/PoolDashboardCpuProvisionning.vue";
|
||||
import PoolDashboardCpuProvisioning from "@/components/pool/dashboard/PoolDashboardCpuProvisioning.vue";
|
||||
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
|
||||
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
|
||||
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<UiTitle type="h4">
|
||||
This helper will generate a basic story component
|
||||
</UiTitle>
|
||||
<div>
|
||||
Choose a component:
|
||||
<select v-model="componentPath">
|
||||
<UiCard class="home-view">
|
||||
<UiCardTitle>Component Story skeleton generator</UiCardTitle>
|
||||
|
||||
<div class="row">
|
||||
Choose a component
|
||||
<FormSelect v-model="componentPath">
|
||||
<option value="" />
|
||||
<option v-for="(component, path) in componentsWithProps" :key="path">
|
||||
<option v-for="path in componentPaths" :key="path">
|
||||
{{ path }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="slots">
|
||||
<label>
|
||||
Slots names, separated by a comma
|
||||
<input v-model="slots" />
|
||||
</label>
|
||||
<button @click="slots = 'default'">Default</button>
|
||||
<button @click="slots = ''">Clear</button>
|
||||
</div>
|
||||
</FormSelect>
|
||||
</div>
|
||||
<CodeHighlight v-if="componentPath" :code="template" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
Slot names, separated by comma
|
||||
<span class="slots">
|
||||
<FormInput v-model="slots" />
|
||||
<UiButton @click="slots = 'default'">Default</UiButton>
|
||||
<UiButton outlined @click="slots = ''">Clear</UiButton>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-for="warning in warnings" :key="warning" class="row warning">
|
||||
<UiIcon :icon="faWarning" />
|
||||
{{ warning }}
|
||||
</p>
|
||||
|
||||
<CodeHighlight
|
||||
class="code-highlight"
|
||||
v-if="componentPath"
|
||||
:code="template"
|
||||
/>
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CodeHighlight from "@/components/CodeHighlight.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { castArray } from "lodash-es";
|
||||
import { type ComponentOptions, computed, ref, watch } from "vue";
|
||||
|
||||
const componentPath = ref("");
|
||||
@@ -44,10 +61,14 @@ const componentsWithProps = Object.fromEntries(
|
||||
)
|
||||
);
|
||||
|
||||
const componentPaths = Object.keys(componentsWithProps);
|
||||
|
||||
const lines = ref<string[]>([]);
|
||||
const slots = ref("");
|
||||
|
||||
const quote = (str: string) => `'${str}'`;
|
||||
const camel = (str: string) =>
|
||||
str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
|
||||
const paramsToImport = ref(new Set<string>());
|
||||
const widgetsToImport = ref(new Set<string>());
|
||||
|
||||
@@ -61,13 +82,15 @@ const template = computed(() => {
|
||||
.filter((name) => name !== "");
|
||||
|
||||
for (const slotName of slotsNames) {
|
||||
paramsLines.push(`slot(${slotName === "default" ? "" : quote(slotName)})`);
|
||||
paramsLines.push(
|
||||
`slot(${slotName === "default" ? "" : quote(camel(slotName))})`
|
||||
);
|
||||
}
|
||||
|
||||
for (const slotName of slotsNames) {
|
||||
paramsLines.push(
|
||||
`setting(${quote(
|
||||
`${slotName}SlotContent`
|
||||
`${camel(slotName)}SlotContent`
|
||||
)}).preset('Example content for ${slotName} slot').widget(text()).help('Content for ${slotName} slot')`
|
||||
);
|
||||
}
|
||||
@@ -78,7 +101,7 @@ const template = computed(() => {
|
||||
}
|
||||
|
||||
const paramsStr = paramsLines.join(",\n ");
|
||||
|
||||
const scriptEndTag = "</" + "script>";
|
||||
return `<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties, settings }"
|
||||
@@ -91,8 +114,10 @@ const template = computed(() => {
|
||||
? `>\n ${slotsNames
|
||||
.map((name) =>
|
||||
name === "default"
|
||||
? `{{ settings.${name}SlotContent }}`
|
||||
: `<template #${name}>{{ settings.${name}SlotContent }}</template>`
|
||||
? `{{ settings.${camel(name)}SlotContent }}`
|
||||
: `<template #${name}>{{ settings.${camel(
|
||||
name
|
||||
)}SlotContent }}</template>`
|
||||
)
|
||||
.join("\n ")}
|
||||
</${componentName}>`
|
||||
@@ -118,10 +143,30 @@ ${
|
||||
)} } from "@/libs/story/story-widget"`
|
||||
: ""
|
||||
}
|
||||
${"<"}/script>
|
||||
${scriptEndTag}
|
||||
`;
|
||||
});
|
||||
|
||||
const warnings = ref(new Set<string>());
|
||||
|
||||
const extractTypeFromConstructor = (
|
||||
ctor: null | (new () => unknown),
|
||||
propName: string
|
||||
) => {
|
||||
if (ctor == null) {
|
||||
warnings.value.add(
|
||||
`An unknown type has been detected for prop "${propName}"`
|
||||
);
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
if (ctor === Date) {
|
||||
return "Date";
|
||||
}
|
||||
|
||||
return ctor.name.toLocaleLowerCase();
|
||||
};
|
||||
|
||||
watch(
|
||||
componentPath,
|
||||
(path: string) => {
|
||||
@@ -133,6 +178,7 @@ watch(
|
||||
slots.value = "";
|
||||
widgetsToImport.value = new Set();
|
||||
paramsToImport.value = new Set();
|
||||
warnings.value = new Set();
|
||||
lines.value = [];
|
||||
|
||||
for (const propName in component.props) {
|
||||
@@ -147,12 +193,14 @@ watch(
|
||||
current.push(`default(${quote(prop.default)})`);
|
||||
}
|
||||
|
||||
if (prop.type) {
|
||||
const type = prop.type();
|
||||
if (prop.type !== undefined) {
|
||||
const type = castArray(prop.type)
|
||||
.map((ctor) => extractTypeFromConstructor(ctor, propName))
|
||||
.join(" | ");
|
||||
|
||||
current.push(
|
||||
`type(${quote(Array.isArray(type) ? "array" : typeof type)})`
|
||||
);
|
||||
if (type !== "unknown") {
|
||||
current.push(`type(${quote(type)})`);
|
||||
}
|
||||
}
|
||||
|
||||
const isModel = component.emits?.includes(`update:${propName}`);
|
||||
@@ -164,17 +212,29 @@ watch(
|
||||
})`
|
||||
);
|
||||
|
||||
current.push("widget()");
|
||||
if (!isModel) {
|
||||
current.push("widget()");
|
||||
}
|
||||
|
||||
lines.value.push(current.join("."));
|
||||
}
|
||||
|
||||
let shouldImportEvent = false;
|
||||
|
||||
if (component.emits) {
|
||||
paramsToImport.value.add("event");
|
||||
for (const eventName of component.emits) {
|
||||
lines.value.push(`event("${eventName}")`);
|
||||
if (eventName.startsWith("update:")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
shouldImportEvent = true;
|
||||
lines.value.push(`event(${quote(eventName)})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldImportEvent) {
|
||||
paramsToImport.value.add("event");
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
@@ -185,11 +245,28 @@ watch(
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.ui-title {
|
||||
margin-bottom: 1rem;
|
||||
.slots {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
|
||||
:deep(input) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.slots {
|
||||
.row {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.warning {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-orange-world-base);
|
||||
}
|
||||
|
||||
.code-highlight {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
7
@xen-orchestra/lite/src/views/vm/VmAlarmsView.vue
Normal file
7
@xen-orchestra/lite/src/views/vm/VmAlarmsView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
||||
</script>
|
||||
7
@xen-orchestra/lite/src/views/vm/VmDashboardView.vue
Normal file
7
@xen-orchestra/lite/src/views/vm/VmDashboardView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
||||
</script>
|
||||
7
@xen-orchestra/lite/src/views/vm/VmNetworkView.vue
Normal file
7
@xen-orchestra/lite/src/views/vm/VmNetworkView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
||||
</script>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<ObjectNotFoundWrapper :is-ready="isReady" :uuid-checker="hasUuid">
|
||||
<VmHeader />
|
||||
<VmTabBar :uuid="vm!.uuid" />
|
||||
<RouterView />
|
||||
</ObjectNotFoundWrapper>
|
||||
</template>
|
||||
@@ -8,18 +9,16 @@
|
||||
<script lang="ts" setup>
|
||||
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
|
||||
import VmHeader from "@/components/vm/VmHeader.vue";
|
||||
import VmTabBar from "@/components/vm/VmTabBar.vue";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { useVmStore } from "@/stores/vm.store";
|
||||
import { watchEffect } from "vue";
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const { getByUuid, hasUuid, isReady } = useVmStore().subscribe();
|
||||
const uiStore = useUiStore();
|
||||
|
||||
watchEffect(() => {
|
||||
uiStore.currentHostOpaqueRef = getByUuid(
|
||||
route.params.uuid as string
|
||||
)?.resident_on;
|
||||
});
|
||||
const vm = computed(() => getByUuid(route.params.uuid as string));
|
||||
whenever(vm, (vm) => (uiStore.currentHostOpaqueRef = vm.resident_on));
|
||||
</script>
|
||||
|
||||
7
@xen-orchestra/lite/src/views/vm/VmStatsView.vue
Normal file
7
@xen-orchestra/lite/src/views/vm/VmStatsView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
||||
</script>
|
||||
7
@xen-orchestra/lite/src/views/vm/VmStorageView.vue
Normal file
7
@xen-orchestra/lite/src/views/vm/VmStorageView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
||||
</script>
|
||||
7
@xen-orchestra/lite/src/views/vm/VmSystemView.vue
Normal file
7
@xen-orchestra/lite/src/views/vm/VmSystemView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
||||
</script>
|
||||
7
@xen-orchestra/lite/src/views/vm/VmTasksView.vue
Normal file
7
@xen-orchestra/lite/src/views/vm/VmTasksView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<PageUnderConstruction />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
||||
</script>
|
||||
@@ -21,7 +21,7 @@
|
||||
"dependencies": {
|
||||
"@vates/event-listeners-manager": "^1.0.1",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@vates/task": "^0.1.1",
|
||||
"@vates/task": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"acme-client": "^5.0.0",
|
||||
"app-conf": "^2.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/proxy",
|
||||
"version": "0.26.21",
|
||||
"version": "0.26.23",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "XO Proxy used to remotely execute backup jobs",
|
||||
"keywords": [
|
||||
@@ -32,7 +32,7 @@
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.4",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.36.0",
|
||||
"@xen-orchestra/backups": "^0.36.1",
|
||||
"@xen-orchestra/fs": "^3.3.4",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@xen-orchestra/mixin": "^0.1.0",
|
||||
@@ -60,7 +60,7 @@
|
||||
"source-map-support": "^0.5.16",
|
||||
"stoppable": "^1.0.6",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.3.0",
|
||||
"xen-api": "^1.3.1",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"pw": "^0.0.4",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-vmdk-to-vhd": "^2.5.3"
|
||||
"xo-vmdk-to-vhd": "^2.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "0.2.2",
|
||||
"name": "@xen-orchestra/vmware-explorer",
|
||||
"dependencies": {
|
||||
"@vates/task": "^0.1.1",
|
||||
"@vates/task": "^0.1.2",
|
||||
"@vates/read-chunk": "^1.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^3.3.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xen-api": "^1.3.0"
|
||||
"xen-api": "^1.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -2,14 +2,56 @@
|
||||
|
||||
## **next**
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [New/VM] Fix stuck Cloud Config import ([GitHub comment](https://github.com/vatesfr/xen-orchestra/issues/5896#issuecomment-1465253774))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api 1.3.1
|
||||
- @xen-orchestra/proxy 0.26.23
|
||||
- xo-server 5.114.2
|
||||
|
||||
## **5.82.1** (2023-05-12)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Plugins] Clicking on a plugin name now filters out other plugins
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Host/Network] Fix IP configuration not working with empty fields
|
||||
- [Import/VM/From VMware] Fix `Property description must be an object: undefined` [Forum#61834](https://xcp-ng.org/forum/post/61834) [Forum#61900](https://xcp-ng.org/forum/post/61900)
|
||||
- [Import/VM/From VMware] Fix `Cannot read properties of undefined (reading 'stream')` [Forum#59879](https://xcp-ng.org/forum/post/59879) (PR [#6825](https://github.com/vatesfr/xen-orchestra/pull/6825))
|
||||
- [OVA export] Fix major memory leak which may lead to xo-server crash [Forum#56051](https://xcp-ng.org/forum/post/56051) (PR [#6800](https://github.com/vatesfr/xen-orchestra/pull/6800))
|
||||
- [VM] Fix `VBD_IS_EMPTY` error when converting to template [Forum#61653](https://xcp-ng.org/forum/post/61653) (PR [#6808](https://github.com/vatesfr/xen-orchestra/pull/6808))
|
||||
- [New/Network] Fix `invalid parameter error` when not providing a VLAN [Forum#62090](https://xcp-ng.org/forum/post/62090) (PR [#6829](https://github.com/vatesfr/xen-orchestra/pull/6829))
|
||||
- [Backup/Health check] Fix `task has already ended` error during a healthcheck in continous replication [Forum#62073](https://xcp-ng.org/forum/post/62073) (PR [#6830](https://github.com/vatesfr/xen-orchestra/pull/6830))
|
||||
|
||||
### Released packages
|
||||
|
||||
- @vates/task 0.1.2
|
||||
- xo-vmdk-to-vhd 2.5.4
|
||||
- @xen-orchestra/backups 0.36.1
|
||||
- @xen-orchestra/proxy 0.26.22
|
||||
- xo-server 5.114.1
|
||||
- xo-web 5.117.1
|
||||
|
||||
## **5.82.0** (2023-04-28)
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Host] Smart reboot: suspend resident VMs, restart host and resume VMs [#6750](https://github.com/vatesfr/xen-orchestra/issues/6750) (PR [#6795](https://github.com/vatesfr/xen-orchestra/pull/6795))
|
||||
- [Backup/exports] Retry when failing to read a data block during Delta Backup, Continuous Replication, disk and OVA export when NBD is enabled [PR #6763](https://github.com/vatesfr/xen-orchestra/pull/6763)
|
||||
- [Backup/Health check] [Opt-in XenStore API](https://xen-orchestra.com/docs/backups.html#backup-health-check) to execute custom checks inside the VM (PR [#6784](https://github.com/vatesfr/xen-orchestra/pull/6784))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM/Advanced] Automatically eject removable medias when converting a VM to a template [#6752](https://github.com/vatesfr/xen-orchestra/issues/6752) (PR [#6769](https://github.com/vatesfr/xen-orchestra/pull/6769))
|
||||
- [Dashboard/Health] Add free space column for storage state table (PR [#6778](https://github.com/vatesfr/xen-orchestra/pull/6778))
|
||||
- [VM/General] Displays the template name used to create the VM, as well as the email address of the VM creator for admin users (PR [#6771](https://github.com/vatesfr/xen-orchestra/pull/6771))
|
||||
- [Backup/exports] Retry when failing to read a data block during Delta Backup, Continuous Replication, disk and OVA export when NBD is enabled [PR #6763](https://github.com/vatesfr/xen-orchestra/pull/6763)
|
||||
- [Host] Smart reboot: suspend resident VMs, restart host and resume VMs [#6750](https://github.com/vatesfr/xen-orchestra/issues/6750) (PR [#6795](https://github.com/vatesfr/xen-orchestra/pull/6795))
|
||||
- [Backup/Health check] [Opt-in XenStore API](https://xen-orchestra.com/docs/backups.html#backup-health-check) to execute custom checks inside the VM (PR [#6784](https://github.com/vatesfr/xen-orchestra/pull/6784))
|
||||
- [Kubernetes] Give the possibility to create an high availability cluster (PR [#6794](https://github.com/vatesfr/xen-orchestra/pull/6794))
|
||||
|
||||
### Bug fixes
|
||||
@@ -41,11 +83,11 @@
|
||||
- @xen-orchestra/backups-cli 1.0.6
|
||||
- @xen-orchestra/proxy 0.26.21
|
||||
- xo-server 5.113.0
|
||||
- xo-web 5.116.0
|
||||
- xo-web 5.116.1
|
||||
|
||||
## **5.81** (2023-03-31)
|
||||
|
||||
<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" />
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -84,8 +126,6 @@
|
||||
|
||||
## **5.80.2** (2023-03-16)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Plugin/auth-oidc] Support `email` for _username field_ setting [Forum#59587](https://xcp-ng.org/forum/post/59587)
|
||||
|
||||
@@ -7,10 +7,15 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Proxy] Make proxy address editable (PR [#6816](https://github.com/vatesfr/xen-orchestra/pull/6816))
|
||||
- [Home/Host] Displays a warning for hosts with HVM disabled [#6823](https://github.com/vatesfr/xen-orchestra/issues/6823) (PR [#6834](https://github.com/vatesfr/xen-orchestra/pull/6834))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Sorted table] In collapsed actions, a spinner is displayed during the action time (PR [#6831](https://github.com/vatesfr/xen-orchestra/pull/6831))
|
||||
|
||||
### Packages to release
|
||||
|
||||
> When modifying a package, add it here with its release type.
|
||||
@@ -27,6 +32,6 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- xo-web patch
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -336,5 +336,5 @@ useSudo = true
|
||||
You need to configure `sudo` to allow the user of your choice to run mount/umount commands without asking for a password. Depending on your operating system / sudo version, the location of this configuration may change. Regardless, you can use:
|
||||
|
||||
```
|
||||
username ALL=(root)NOPASSWD: /bin/mount, /bin/umount
|
||||
username ALL=(root)NOPASSWD: /bin/mount, /bin/umount, /bin/findmnt
|
||||
```
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"prepare": "husky install",
|
||||
"prettify": "prettier --ignore-path .gitignore --ignore-unknown --write .",
|
||||
"test": "npm run test-lint && npm run test-unit",
|
||||
"test-integration": "jest \".integ\\.spec\\.js$\"",
|
||||
"test-integration": "jest \".integ\\.spec\\.js$\" && scripts/run-script.js --parallel test-integration",
|
||||
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
|
||||
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js --bail test"
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test"
|
||||
"test-integration": "node--test *.integ.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"execa": "^4.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
const { readChunkStrict, skipStrict } = require('@vates/read-chunk')
|
||||
const { Readable } = require('node:stream')
|
||||
const { unpackHeader } = require('./Vhd/_utils')
|
||||
const { unpackHeader, unpackFooter } = require('./Vhd/_utils')
|
||||
const {
|
||||
FOOTER_SIZE,
|
||||
HEADER_SIZE,
|
||||
@@ -24,6 +24,7 @@ exports.createNbdRawStream = async function createRawStream(nbdClient) {
|
||||
|
||||
exports.createNbdVhdStream = async function createVhdStream(nbdClient, sourceStream) {
|
||||
const bufFooter = await readChunkStrict(sourceStream, FOOTER_SIZE)
|
||||
const footer = unpackFooter(bufFooter)
|
||||
|
||||
const header = unpackHeader(await readChunkStrict(sourceStream, HEADER_SIZE))
|
||||
// compute BAT in order
|
||||
@@ -70,19 +71,20 @@ exports.createNbdVhdStream = async function createVhdStream(nbdClient, sourceStr
|
||||
}
|
||||
|
||||
async function* iterator() {
|
||||
yield bufFooter
|
||||
yield rawHeader
|
||||
yield bat
|
||||
yield { buffer: bufFooter, type: 'footer', footer }
|
||||
yield { buffer: rawHeader, type: 'header', header }
|
||||
yield { buffer: bat, type: 'bat' }
|
||||
|
||||
let precBlocOffset = FOOTER_SIZE + HEADER_SIZE + batSize
|
||||
for (let i = 0; i < PARENT_LOCATOR_ENTRIES; i++) {
|
||||
const parentLocatorOffset = header.parentLocatorEntry[i].platformDataOffset
|
||||
const space = header.parentLocatorEntry[i].platformDataSpace * SECTOR_SIZE
|
||||
const parentLocatorEntry = header.parentLocatorEntry[i]
|
||||
const parentLocatorOffset = parentLocatorEntry.platformDataOffset
|
||||
const space = parentLocatorEntry.platformDataSpace * SECTOR_SIZE
|
||||
if (space > 0) {
|
||||
await skipStrict(sourceStream, parentLocatorOffset - precBlocOffset)
|
||||
const data = await readChunkStrict(sourceStream, space)
|
||||
precBlocOffset = parentLocatorOffset + space
|
||||
yield data
|
||||
yield { ...parentLocatorEntry, buffer: data, type: 'parentLocator', id: i }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,16 +98,25 @@ exports.createNbdVhdStream = async function createVhdStream(nbdClient, sourceStr
|
||||
}
|
||||
})
|
||||
const bitmap = Buffer.alloc(SECTOR_SIZE, 255)
|
||||
let index = 0
|
||||
for await (const block of nbdIterator) {
|
||||
yield bitmap // don't forget the bitmap before the block
|
||||
yield block
|
||||
const buffer = Buffer.concat([bitmap, block])
|
||||
yield { buffer, type: 'block', id: entries[index] }
|
||||
index++
|
||||
}
|
||||
yield bufFooter
|
||||
yield { buffer: bufFooter, type: 'footer', footer }
|
||||
}
|
||||
|
||||
const stream = Readable.from(iterator())
|
||||
async function* bufferIterator() {
|
||||
for await (const { buffer } of iterator()) {
|
||||
yield buffer
|
||||
}
|
||||
}
|
||||
|
||||
const stream = Readable.from(bufferIterator())
|
||||
stream.length = (offsetSector + blockSizeInSectors + 1) /* end footer */ * SECTOR_SIZE
|
||||
stream._nbd = true
|
||||
stream._iterator = iterator
|
||||
stream.on('error', () => nbdClient.disconnect())
|
||||
stream.on('end', () => nbdClient.disconnect())
|
||||
return stream
|
||||
|
||||
@@ -160,7 +160,12 @@ class StreamParser {
|
||||
yield* this.blocks()
|
||||
}
|
||||
}
|
||||
|
||||
exports.parseVhdStream = async function* parseVhdStream(stream) {
|
||||
const parser = new StreamParser(stream)
|
||||
yield* parser.parse()
|
||||
if (stream._iterator) {
|
||||
yield* stream._iterator()
|
||||
} else {
|
||||
const parser = new StreamParser(stream)
|
||||
yield* parser.parse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^1.3.0"
|
||||
"xen-api": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xen-api",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
@@ -35,7 +35,7 @@
|
||||
"bind-property-descriptor": "^2.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"debug": "^4.0.1",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "^1.0.2",
|
||||
"jest-diff": "^29.0.3",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"kindof": "^2.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.113.0",
|
||||
"version": "5.114.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vates/predicates": "^1.1.0",
|
||||
"@vates/read-chunk": "^1.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.36.0",
|
||||
"@xen-orchestra/backups": "^0.36.1",
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/defined": "^0.0.1",
|
||||
"@xen-orchestra/emit-async": "^1.0.0",
|
||||
@@ -131,12 +131,12 @@
|
||||
"vhd-lib": "^4.4.0",
|
||||
"ws": "^8.2.3",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.3.0",
|
||||
"xen-api": "^1.3.1",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.5.0",
|
||||
"xo-common": "^0.8.0",
|
||||
"xo-remote-parser": "^0.9.2",
|
||||
"xo-vmdk-to-vhd": "^2.5.3"
|
||||
"xo-vmdk-to-vhd": "^2.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function create({ pool, name, description, pif, mtu = 1500, vlan =
|
||||
description,
|
||||
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
||||
mtu: +mtu,
|
||||
vlan: +vlan,
|
||||
vlan,
|
||||
})
|
||||
|
||||
if (nbd) {
|
||||
@@ -27,7 +27,7 @@ create.params = {
|
||||
description: { type: 'string', minLength: 0, optional: true },
|
||||
pif: { type: 'string', optional: true },
|
||||
mtu: { type: 'integer', optional: true },
|
||||
vlan: { type: ['integer', 'string'], optional: true },
|
||||
vlan: { type: 'integer', optional: true },
|
||||
}
|
||||
|
||||
create.resolve = {
|
||||
|
||||
@@ -84,10 +84,10 @@ export async function reconfigureIp({ pif, mode = 'DHCP', ip = '', netmask = '',
|
||||
reconfigureIp.params = {
|
||||
id: { type: 'string', optional: true },
|
||||
mode: { type: 'string', optional: true },
|
||||
ip: { type: 'string', optional: true },
|
||||
netmask: { type: 'string', optional: true },
|
||||
gateway: { type: 'string', optional: true },
|
||||
dns: { type: 'string', optional: true },
|
||||
ip: { type: 'string', minLength: 0, optional: true },
|
||||
netmask: { type: 'string', minLength: 0, optional: true },
|
||||
gateway: { type: 'string', minLength: 0, optional: true },
|
||||
dns: { type: 'string', minLength: 0, optional: true },
|
||||
}
|
||||
|
||||
reconfigureIp.resolve = {
|
||||
|
||||
@@ -186,6 +186,12 @@ export const create = defer(async function ($defer, params) {
|
||||
}
|
||||
}
|
||||
|
||||
const resourceSetTags = resourceSet !== undefined ? (await this.getResourceSet(resourceSet)).tags : undefined
|
||||
const paramsTags = params.tags
|
||||
if (resourceSetTags !== undefined) {
|
||||
params.tags = paramsTags !== undefined ? paramsTags.concat(resourceSetTags) : resourceSetTags
|
||||
}
|
||||
|
||||
const xapiVm = await xapi.createVm(template._xapiId, params, checkLimits, user.id)
|
||||
$defer.onFailure(() => xapi.VM_destroy(xapiVm.$ref, { deleteDisks: true, force: true }))
|
||||
|
||||
@@ -877,7 +883,8 @@ export async function convertToTemplate({ vm }) {
|
||||
|
||||
// Attempts to eject all removable media
|
||||
const ignoreNotRemovable = error => {
|
||||
if (error.code !== 'VBD_NOT_REMOVABLE_MEDIA') {
|
||||
const { code } = error
|
||||
if (code !== 'VBD_IS_EMPTY' && code !== 'VBD_NOT_REMOVABLE_MEDIA') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1369,7 +1376,7 @@ export async function importMultipleFromEsxi({
|
||||
await asyncEach(
|
||||
vms,
|
||||
async vm => {
|
||||
await new Task({ name: `importing vm ${vm}` }).run(async () => {
|
||||
await Task.run({ data: { name: `importing vm ${vm}` } }, async () => {
|
||||
try {
|
||||
const vmUuid = await this.migrationfromEsxi({
|
||||
host,
|
||||
|
||||
@@ -154,7 +154,7 @@ export default class MigrateVm {
|
||||
}
|
||||
|
||||
#connectToEsxi(host, user, password, sslVerify) {
|
||||
return new Task({ name: `connecting to ${host}` }).run(async () => {
|
||||
return Task.run({ data: { name: `connecting to ${host}` } }, async () => {
|
||||
const esxi = new Esxi(host, user, password, sslVerify)
|
||||
await fromEvent(esxi, 'ready')
|
||||
return esxi
|
||||
@@ -174,21 +174,24 @@ export default class MigrateVm {
|
||||
const app = this._app
|
||||
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
|
||||
|
||||
const esxiVmMetadata = await new Task({ name: `get metadata of ${vmId}` }).run(async () => {
|
||||
const esxiVmMetadata = await Task.run({ data: { name: `get metadata of ${vmId}` } }, async () => {
|
||||
return esxi.getTransferableVmMetadata(vmId)
|
||||
})
|
||||
|
||||
const { disks, firmware, memory, name_label, networks, nCpus, powerState, snapshots } = esxiVmMetadata
|
||||
const isRunning = powerState !== 'poweredOff'
|
||||
|
||||
const chainsByNodes = await new Task({ name: `build disks and snapshots chains for ${vmId}` }).run(async () => {
|
||||
return this.#buildDiskChainByNode(disks, snapshots)
|
||||
})
|
||||
const chainsByNodes = await Task.run(
|
||||
{ data: { name: `build disks and snapshots chains for ${vmId}` } },
|
||||
async () => {
|
||||
return this.#buildDiskChainByNode(disks, snapshots)
|
||||
}
|
||||
)
|
||||
|
||||
const sr = app.getXapiObject(srId)
|
||||
const xapi = sr.$xapi
|
||||
|
||||
const vm = await new Task({ name: 'creating MV on XCP side ' }).run(async () => {
|
||||
const vm = await Task.run({ data: { name: 'creating MV on XCP side' } }, async () => {
|
||||
// got data, ready to start creating
|
||||
const vm = await xapi._getOrWaitObject(
|
||||
await xapi.VM_create({
|
||||
@@ -233,7 +236,7 @@ export default class MigrateVm {
|
||||
|
||||
const vhds = await Promise.all(
|
||||
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
||||
new Task({ name: `Cold import of disks ${node} ` }).run(async () => {
|
||||
Task.run({ data: { name: `Cold import of disks ${node}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const vdi = await xapi._getOrWaitObject(
|
||||
await xapi.VDI_create({
|
||||
@@ -268,9 +271,11 @@ export default class MigrateVm {
|
||||
}
|
||||
parentVhd = vhd
|
||||
}
|
||||
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
if (vhd !== undefined) {
|
||||
// it can be empty if the VM don't have a snapshot and is running
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
}
|
||||
return { vdi, vhd }
|
||||
})
|
||||
)
|
||||
@@ -278,20 +283,26 @@ export default class MigrateVm {
|
||||
|
||||
if (isRunning && stopSource) {
|
||||
// it the vm was running, we stop it and transfer the data in the active disk
|
||||
await new Task({ name: 'powering down source VM' }).run(() => esxi.powerOff(vmId))
|
||||
await Task.run({ data: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(chainsByNodes).map(async (node, userdevice) => {
|
||||
await new Task({ name: `Transfering deltas of ${userdevice}` }).run(async () => {
|
||||
await Task.run({ data: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
||||
const chainByNode = chainsByNodes[node]
|
||||
const disk = chainByNode[chainByNode.length - 1]
|
||||
const { fileName, path, datastore, isFull } = disk
|
||||
const { vdi, vhd: parentVhd } = vhds[userdevice]
|
||||
let vhd
|
||||
if (vdi === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent vdi`)
|
||||
}
|
||||
if (isFull) {
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
||||
await vhd.readBlockAllocationTable()
|
||||
} else {
|
||||
if (parentVhd === undefined) {
|
||||
throw new Error(`Can't import delta of a running VM without its parent VHD`)
|
||||
}
|
||||
// we only want to transfer blocks present in the delta vhd, not the full vhd chain
|
||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, {
|
||||
lookMissingBlockInParent: false,
|
||||
@@ -305,7 +316,7 @@ export default class MigrateVm {
|
||||
)
|
||||
}
|
||||
|
||||
await new Task({ name: 'Finishing transfer' }).run(async () => {
|
||||
await Task.run({ data: { name: 'Finishing transfer' } }, async () => {
|
||||
// remove the importing in label
|
||||
await vm.set_name_label(esxiVmMetadata.name_label)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-vmdk-to-vhd",
|
||||
"version": "2.5.3",
|
||||
"version": "2.5.4",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "JS lib reading and writing .vmdk and .ova files",
|
||||
"keywords": [
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function writeOvaOn(
|
||||
|
||||
async function writeDisk(entry, blockIterator) {
|
||||
for await (const block of blockIterator) {
|
||||
entry.write(block)
|
||||
await fromCallback.call(entry, entry.write, block)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.116.0",
|
||||
"version": "5.117.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -137,7 +137,7 @@
|
||||
"xo-common": "^0.8.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-remote-parser": "^0.9.2",
|
||||
"xo-vmdk-to-vhd": "^2.5.3"
|
||||
"xo-vmdk-to-vhd": "^2.5.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",
|
||||
|
||||
@@ -917,6 +917,7 @@ const messages = {
|
||||
|
||||
// ----- Host item ------
|
||||
host: 'Host',
|
||||
hostHvmDisabled: 'Hardware-assisted virtualization is not enabled on this host',
|
||||
hostNoLicensePartialProSupport:
|
||||
'This host does not have an active license, even though it is in a pool with licensed hosts. In order for XCP-ng Pro Support to be enabled on a pool, all hosts within the pool must have an active license',
|
||||
hostNoSupport: 'No XCP-ng Pro Support enabled on this host',
|
||||
|
||||
@@ -168,8 +168,12 @@ const handleFnProps = (prop, items, userData) => (typeof prop === 'function' ? p
|
||||
const CollapsedActions = decorate([
|
||||
withRouter,
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
runningActions: [],
|
||||
}),
|
||||
effects: {
|
||||
async execute(state, { handler, label, redirectOnSuccess }) {
|
||||
this.state.runningActions = [...this.state.runningActions, label]
|
||||
try {
|
||||
await handler()
|
||||
ifDef(redirectOnSuccess, this.props.router.push)
|
||||
@@ -183,18 +187,25 @@ const CollapsedActions = decorate([
|
||||
_error(label, defined(error.message, String(error)))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.state.runningActions = this.state.runningActions.filter(action => action !== label)
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
wrappedActions: ({ runningActions }, { actions }) =>
|
||||
actions.map(action => {
|
||||
action.isRunning = runningActions.includes(action.label)
|
||||
return action
|
||||
}),
|
||||
dropdownId: generateId,
|
||||
actions: (_, { actions, items, userData }) =>
|
||||
actions.map(({ disabled, grouped, handler, icon, label, level, redirectOnSuccess }) => {
|
||||
actions: ({ wrappedActions: actions }, { items, userData }) =>
|
||||
actions.map(({ disabled, grouped, handler, icon, isRunning, label, level, redirectOnSuccess }) => {
|
||||
const actionItems = Array.isArray(items) || !grouped ? items : [items]
|
||||
return {
|
||||
disabled: handleFnProps(disabled, actionItems, userData),
|
||||
disabled: isRunning || handleFnProps(disabled, actionItems, userData),
|
||||
handler: () => handler(actionItems, userData),
|
||||
icon: handleFnProps(icon, actionItems, userData),
|
||||
icon: isRunning ? 'loading' : handleFnProps(icon, actionItems, userData),
|
||||
label: handleFnProps(label, actionItems, userData),
|
||||
level: handleFnProps(level, actionItems, userData),
|
||||
redirectOnSuccess: handleFnProps(redirectOnSuccess, actionItems, userData),
|
||||
|
||||
@@ -177,6 +177,17 @@ export default class HostItem extends Component {
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (!host.hvmCapable) {
|
||||
alerts.push({
|
||||
level: 'warning',
|
||||
render: (
|
||||
<span>
|
||||
<Icon icon='alarm' /> {_('hostHvmDisabled')}
|
||||
</span>
|
||||
),
|
||||
})
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
)
|
||||
|
||||
@@ -197,11 +197,11 @@ const NewNetwork = decorate([
|
||||
networks,
|
||||
pif,
|
||||
pifs,
|
||||
vlan,
|
||||
} = state
|
||||
|
||||
let { mtu } = state
|
||||
let { mtu, vlan } = state
|
||||
mtu = mtu === '' ? undefined : +mtu
|
||||
vlan = vlan === '' ? undefined : +vlan
|
||||
|
||||
return bonded
|
||||
? createBondedNetwork({
|
||||
|
||||
@@ -37,7 +37,13 @@ import { updateApplianceSettings } from './update-appliance-settings'
|
||||
import Tooltip from '../../common/tooltip'
|
||||
import { getXoaPlan, SOURCES } from '../../common/xoa-plans'
|
||||
|
||||
const _editProxy = (value, { name, proxy }) => editProxyAppliance(proxy, { [name]: value })
|
||||
const _editProxy = (value, { name, proxy }) => {
|
||||
if (typeof value === 'string') {
|
||||
value = value.trim()
|
||||
value = value === '' ? null : value
|
||||
}
|
||||
return editProxyAppliance(proxy, { [name]: value })
|
||||
}
|
||||
|
||||
const HEADER = (
|
||||
<h2>
|
||||
@@ -143,6 +149,12 @@ const COLUMNS = [
|
||||
itemRenderer: proxy => <Vm id={proxy.vmUuid} link />,
|
||||
name: _('vm'),
|
||||
},
|
||||
{
|
||||
itemRenderer: proxy => (
|
||||
<Text data-name='address' data-proxy={proxy} value={proxy.address ?? ''} onChange={_editProxy} />
|
||||
),
|
||||
name: _('address'),
|
||||
},
|
||||
{
|
||||
name: _('license'),
|
||||
itemRenderer: (proxy, { isAdmin, licensesByVmUuid }) => {
|
||||
|
||||
@@ -5,9 +5,11 @@ import ActionToggle from 'action-toggle'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import decorate from 'apply-decorators'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import GenericInput from 'json-schema-input'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import pFinally from 'promise-toolbox/finally'
|
||||
@@ -39,6 +41,17 @@ class Plugin extends Component {
|
||||
this.testFormId = `form-test-${props.id}`
|
||||
}
|
||||
|
||||
_getPluginLink = createSelector(
|
||||
() => this.props.name,
|
||||
name => {
|
||||
const s = new ComplexMatcher.Property(
|
||||
'name',
|
||||
new ComplexMatcher.RegExp('^' + escapeRegExp(name) + '$', 'i')
|
||||
).toString()
|
||||
return location => ({ ...location, query: { ...location.query, s } })
|
||||
}
|
||||
)
|
||||
|
||||
_getUiSchema = createSelector(() => this.props.configurationSchema, generateUiSchema)
|
||||
|
||||
_updateExpanded = () => {
|
||||
@@ -125,9 +138,8 @@ class Plugin extends Component {
|
||||
<Row>
|
||||
<Col mediumSize={8}>
|
||||
<h5 className='form-inline clearfix'>
|
||||
<ActionToggle disabled={loaded && props.unloadable === false} handler={this._updateLoad} value={loaded} />
|
||||
<span className='text-primary'>{` ${props.name} `}</span>
|
||||
<span>{`(v${props.version}) `}</span>
|
||||
<ActionToggle disabled={loaded && props.unloadable === false} handler={this._updateLoad} value={loaded} />{' '}
|
||||
<Link to={this._getPluginLink()}>{props.name}</Link> <span>{`(v${props.version}) `}</span>
|
||||
{description !== undefined && description !== '' && (
|
||||
<span className='text-muted small'> - {description}</span>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,8 @@ example.{,c,m}js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.integ.{,c,m}js
|
||||
*.integ.{,c,m}js.map
|
||||
*.spec.{,c,m}js
|
||||
*.spec.{,c,m}js.map
|
||||
*.test.{,c,m}js
|
||||
|
||||
@@ -11236,6 +11236,13 @@ http-request-plus@^1.0.0:
|
||||
dependencies:
|
||||
"@xen-orchestra/log" "^0.6.0"
|
||||
|
||||
http-request-plus@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/http-request-plus/-/http-request-plus-1.0.2.tgz#e0d6b4fa79c82f1f9df7dcd1ce62c26934fa20ca"
|
||||
integrity sha512-no2XjPTwCGwzgF+abs3o76e+J2Fm2w9xnUu/nlMbOIPrqooHGPKfpM7uv+QsGMKilFrchJQpvc1NLBfb9VI14Q==
|
||||
dependencies:
|
||||
"@xen-orchestra/log" "^0.6.0"
|
||||
|
||||
http-server-plus@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-server-plus/-/http-server-plus-1.0.0.tgz#cb7adc1ca3e679e8728286a6c9b5b1bd012ccbfd"
|
||||
|
||||
Reference in New Issue
Block a user