Compare commits
2 Commits
fix-refs-b
...
putResourc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35ca5667d9 | ||
|
|
e9d9fbbf91 |
@@ -22,7 +22,7 @@
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^1.2.5"
|
||||
"xen-api": "^1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0",
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Read a chunk of data from a stream.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to read from.
|
||||
* @param {number} size - The number of bytes to read.
|
||||
* @returns {Promise<Buffer|null>} - A Promise that resolves to a Buffer of up to size bytes if available, or null if end of stream is reached. The Promise is rejected if there is an error while reading from the stream.
|
||||
*/
|
||||
const readChunk = (stream, size) =>
|
||||
stream.closed || stream.readableEnded
|
||||
? Promise.resolve(null)
|
||||
@@ -40,13 +33,6 @@ const readChunk = (stream, size) =>
|
||||
})
|
||||
exports.readChunk = readChunk
|
||||
|
||||
/**
|
||||
* Read a chunk of data from a stream.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to read from.
|
||||
* @param {number} size - The number of bytes to read.
|
||||
* @returns {Promise<Buffer>} - A Promise that resolves to a Buffer of size bytes. The Promise is rejected if there is an error while reading from the stream.
|
||||
*/
|
||||
exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
const chunk = await readChunk(stream, size)
|
||||
if (chunk === null) {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.5.1",
|
||||
"xen-api": "^1.2.5"
|
||||
"xen-api": "^1.2.3"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -24,14 +24,12 @@
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"echarts": "^5.3.3",
|
||||
"highlight.js": "^11.6.0",
|
||||
"human-format": "^1.0.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json5": "^2.2.1",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"make-error": "^1.3.6",
|
||||
"markdown-it": "^13.0.1",
|
||||
"pinia": "^2.0.14",
|
||||
"placement.js": "^1.0.0-beta.5",
|
||||
"vue": "^3.2.37",
|
||||
|
||||
@@ -16,8 +16,8 @@ a {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
code, code * {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.card-view {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div ref="rootElement" class="app-markdown" v-html="html" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Ref, computed, ref } from "vue";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import { markdown } from "@/libs/markdown";
|
||||
|
||||
const rootElement = ref() as Ref<HTMLElement>;
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
}>();
|
||||
|
||||
const html = computed(() => markdown.render(props.content ?? ""));
|
||||
|
||||
useEventListener(
|
||||
rootElement,
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (!target.classList.contains("copy-button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const copyable =
|
||||
target.parentElement!.querySelector<HTMLElement>(".copyable");
|
||||
|
||||
if (copyable !== null) {
|
||||
navigator.clipboard.writeText(copyable.innerText);
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.app-markdown {
|
||||
font-size: 1.6rem;
|
||||
|
||||
:deep() {
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1.4rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
code:not(.hljs-code) {
|
||||
background-color: var(--background-color-extra-blue);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: revert;
|
||||
padding-left: 2rem;
|
||||
list-style-type: revert;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
thead th {
|
||||
border-bottom: 2px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 1rem;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-d20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<pre class="code-highlight hljs"><code v-html="codeAsHtml"></code></pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HLJS from "highlight.js";
|
||||
import { computed } from "vue";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
code?: any;
|
||||
lang?: string;
|
||||
}>(),
|
||||
{ lang: "typescript" }
|
||||
);
|
||||
|
||||
const codeAsText = computed(() => {
|
||||
switch (typeof props.code) {
|
||||
case "string":
|
||||
return props.code;
|
||||
case "function":
|
||||
return String(props.code);
|
||||
default:
|
||||
return JSON.stringify(props.code, undefined, 2);
|
||||
}
|
||||
});
|
||||
|
||||
const codeAsHtml = computed(
|
||||
() => HLJS.highlight(codeAsText.value, { language: props.lang }).value
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.code-highlight {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.6rem;
|
||||
text-align: left;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
v-slot="{ isActive, href }"
|
||||
:to="disabled || isTabBarDisabled ? '' : to"
|
||||
custom
|
||||
>
|
||||
<UiTab :active="isActive" :disabled="disabled" :href="href" tag="a">
|
||||
<slot />
|
||||
</UiTab>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import UiTab from "@/components/ui/UiTab.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
to: RouteLocationRaw;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const isTabBarDisabled = inject<ComputedRef<boolean>>(
|
||||
"isTabBarDisabled",
|
||||
computed(() => false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,29 +1,16 @@
|
||||
<template>
|
||||
<div class="ui-tab-bar">
|
||||
<div class="tab-bar">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
provide(
|
||||
"isTabBarDisabled",
|
||||
computed(() => props.disabled)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-tab-bar {
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 6.5rem;
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-primary);
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
54
@xen-orchestra/lite/src/components/TabBarItem.vue
Normal file
54
@xen-orchestra/lite/src/components/TabBarItem.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<span v-if="disabled" class="tab-bar-item disabled">
|
||||
<slot />
|
||||
</span>
|
||||
<RouterLink v-else class="tab-bar-item" v-bind="$props">
|
||||
<slot />
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouterLinkProps } from "vue-router";
|
||||
|
||||
// https://vuejs.org/api/sfc-script-setup.html#type-only-props-emit-declarations
|
||||
interface Props extends RouterLinkProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.tab-bar-item {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.2em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-blue-scale-100);
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--color-blue-scale-400);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,6 @@
|
||||
>
|
||||
<input
|
||||
v-model="value"
|
||||
:class="{ indeterminate: type === 'checkbox' && value === undefined }"
|
||||
:disabled="isLabelDisabled || disabled"
|
||||
:type="type === 'radio' ? 'radio' : 'checkbox'"
|
||||
class="input"
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
inject,
|
||||
ref,
|
||||
} from "vue";
|
||||
import { faCheck, faCircle, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
@@ -54,17 +53,7 @@ const value = useVModel(props, "modelValue", emit);
|
||||
const type = inject<"checkbox" | "radio" | "toggle">("inputType", "checkbox");
|
||||
const hasLabel = inject("hasLabel", false);
|
||||
const isLabelDisabled = inject("isLabelDisabled", ref(false));
|
||||
const icon = computed(() => {
|
||||
if (type !== "checkbox") {
|
||||
return faCircle;
|
||||
}
|
||||
|
||||
if (value.value === undefined) {
|
||||
return faMinus;
|
||||
}
|
||||
|
||||
return faCheck;
|
||||
});
|
||||
const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -85,11 +74,6 @@ const icon = computed(() => {
|
||||
.form-checkbox {
|
||||
--checkbox-border-radius: 0.25em;
|
||||
--checkbox-icon-size: 1em;
|
||||
|
||||
.input.indeterminate + .fake-checkbox > .icon {
|
||||
opacity: 1;
|
||||
color: var(--color-blue-scale-300);
|
||||
}
|
||||
}
|
||||
|
||||
.form-checkbox,
|
||||
@@ -125,8 +109,8 @@ const icon = computed(() => {
|
||||
}
|
||||
|
||||
.icon {
|
||||
transition: transform 0.125s ease-in-out;
|
||||
transform: translateX(-0.7em);
|
||||
transition: transform 0.125s ease-in-out;
|
||||
}
|
||||
|
||||
.input:checked + .fake-checkbox > .icon {
|
||||
@@ -156,12 +140,12 @@ const icon = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.25em;
|
||||
transition: background-color 0.125s ease-in-out,
|
||||
border-color 0.125s ease-in-out;
|
||||
border: var(--checkbox-border-width) solid var(--border-color);
|
||||
border-radius: var(--checkbox-border-radius);
|
||||
background-color: var(--background-color);
|
||||
box-shadow: var(--shadow-100);
|
||||
transition: background-color 0.125s ease-in-out,
|
||||
border-color 0.125s ease-in-out;
|
||||
|
||||
--border-color: var(--color-blue-scale-400);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<span :class="wrapperClass" v-bind="wrapperAttrs">
|
||||
<template v-if="inputType === 'select'">
|
||||
<input
|
||||
v-if="!isSelect"
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="input"
|
||||
ref="inputElement"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<template v-else>
|
||||
<select
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
@@ -15,24 +24,6 @@
|
||||
<UiIcon :fixed-width="false" :icon="faAngleDown" />
|
||||
</span>
|
||||
</template>
|
||||
<textarea
|
||||
v-else-if="inputType === 'textarea'"
|
||||
ref="textarea"
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="textarea"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="input"
|
||||
ref="inputElement"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<span v-if="before !== undefined" class="before">
|
||||
<template v-if="typeof before === 'string'">{{ before }}</template>
|
||||
<UiIcon v-else :icon="before" class="before" />
|
||||
@@ -52,19 +43,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isEmpty } from "lodash-es";
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type InputHTMLAttributes,
|
||||
computed,
|
||||
inject,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import type { Color } from "@/types";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useTextareaAutosize, useVModel } from "@vueuse/core";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
// Temporary workaround for https://github.com/vuejs/core/issues/4294
|
||||
@@ -89,10 +79,8 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
const isEmpty = computed(
|
||||
() => props.modelValue == null || String(props.modelValue).trim() === ""
|
||||
);
|
||||
const inputType = inject("inputType", "input");
|
||||
const empty = computed(() => isEmpty(props.modelValue));
|
||||
const isSelect = inject("isSelect", false);
|
||||
const isLabelDisabled = inject("isLabelDisabled", ref(false));
|
||||
const color = inject(
|
||||
"color",
|
||||
@@ -100,10 +88,10 @@ const color = inject(
|
||||
);
|
||||
|
||||
const wrapperClass = computed(() => [
|
||||
`form-${inputType}`,
|
||||
isSelect ? "form-select" : "form-input",
|
||||
{
|
||||
disabled: props.disabled || isLabelDisabled.value,
|
||||
empty: isEmpty.value,
|
||||
empty: empty.value,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -116,12 +104,6 @@ const inputClass = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const { textarea, triggerResize } = useTextareaAutosize();
|
||||
|
||||
watch(value, () => nextTick(() => triggerResize()), {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const focus = () => inputElement.value.focus();
|
||||
|
||||
defineExpose({
|
||||
@@ -131,13 +113,12 @@ defineExpose({
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
.form-select {
|
||||
display: inline-grid;
|
||||
align-items: stretch;
|
||||
|
||||
--before-width: v-bind('beforeWidth || "1.75em"');
|
||||
--after-width: v-bind('afterWidth || "1.625em"');
|
||||
--before-width: v-bind('beforeWidth ?? "1.75em"');
|
||||
--after-width: v-bind('afterWidth ?? "1.625em"');
|
||||
--caret-width: 1.5em;
|
||||
|
||||
--text-color: var(--color-blue-scale-100);
|
||||
@@ -151,8 +132,7 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
.form-input {
|
||||
grid-template-columns: var(--before-width) auto var(--after-width);
|
||||
}
|
||||
|
||||
@@ -165,10 +145,8 @@ defineExpose({
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea,
|
||||
.select {
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
@@ -257,19 +235,8 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
height: auto;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea {
|
||||
padding-right: 0.625em;
|
||||
padding-left: 0.625em;
|
||||
padding: 0 0.625em 0 0.625em;
|
||||
|
||||
&.has-before {
|
||||
padding-left: calc(var(--before-width) + 0.25em);
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<UiModal
|
||||
@submit.prevent="saveJson"
|
||||
:color="isJsonValid ? 'success' : 'error'"
|
||||
v-if="isCodeModalOpen"
|
||||
:icon="faCode"
|
||||
@close="closeCodeModal"
|
||||
>
|
||||
<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>
|
||||
</UiModal>
|
||||
<FormInput
|
||||
@click="openCodeModal"
|
||||
:model-value="jsonValue"
|
||||
:before="faCode"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormTextarea from "@/components/form/FormTextarea.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";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: any): void;
|
||||
}>();
|
||||
|
||||
const model = useVModel(props, "modelValue", emit);
|
||||
|
||||
const {
|
||||
open: openCodeModal,
|
||||
close: closeCodeModal,
|
||||
isOpen: isCodeModalOpen,
|
||||
} = useModal();
|
||||
|
||||
const jsonValue = computed(() => JSON.stringify(model.value, undefined, 2));
|
||||
|
||||
const isJsonValid = computed(() => {
|
||||
try {
|
||||
JSON.parse(editedJson.value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const formatJson = () => {
|
||||
if (!isJsonValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
editedJson.value = JSON.stringify(JSON.parse(editedJson.value), undefined, 2);
|
||||
};
|
||||
|
||||
const saveJson = () => {
|
||||
if (!isJsonValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formatJson();
|
||||
|
||||
model.value = JSON.parse(editedJson.value);
|
||||
|
||||
closeCodeModal();
|
||||
};
|
||||
|
||||
whenever(isCodeModalOpen, () => (editedJson.value = jsonValue.value));
|
||||
|
||||
const editedJson = ref();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
:deep(.modal-textarea) {
|
||||
min-width: 50rem;
|
||||
min-height: 20rem;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
import { provide } from "vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
|
||||
provide("inputType", "select");
|
||||
provide("isSelect", true);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<FormInput />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { provide } from "vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
|
||||
provide("inputType", "textarea");
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,40 +1,67 @@
|
||||
<template>
|
||||
<UiTabBar :disabled="!isReady">
|
||||
<RouterTab :to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }">
|
||||
<TabBar>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("dashboard") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.alarms', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.alarms', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("alarms") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.stats', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.stats', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("stats") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.system', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.system', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("system") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.network', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.network', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("network") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.storage', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.storage', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("storage") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.tasks', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.tasks', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("tasks") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.hosts', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.hosts', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("hosts") }}
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.vms', params: { uuid: poolUuid } }">
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.vms', params: { uuid: poolUuid } }"
|
||||
>
|
||||
{{ $t("vms") }}
|
||||
</RouterTab>
|
||||
</UiTabBar>
|
||||
</TabBarItem>
|
||||
</TabBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import RouterTab from "@/components/RouterTab.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
import TabBar from "@/components/TabBar.vue";
|
||||
import TabBarItem from "@/components/TabBarItem.vue";
|
||||
import { usePoolStore } from "@/stores/pool.store";
|
||||
|
||||
const poolStore = usePoolStore();
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
:class="{ active, disabled: disabled || isTabBarDisabled }"
|
||||
class="ui-tab"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
tag?: string;
|
||||
}>(),
|
||||
{ tag: "span" }
|
||||
);
|
||||
|
||||
const isTabBarDisabled = inject<ComputedRef<boolean>>(
|
||||
"isTabBarDisabled",
|
||||
computed(() => false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-tab {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.2em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-blue-scale-100);
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--color-blue-scale-400);
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
import HLJS from "highlight.js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
export const markdown = new MarkdownIt();
|
||||
|
||||
markdown.set({
|
||||
highlight: (str: string, lang: string) => {
|
||||
const code = highlight(str, lang);
|
||||
return `<pre class="hljs"><button class="copy-button" type="button">Copy</button><code class="hljs-code">${code}</code></pre>`;
|
||||
},
|
||||
});
|
||||
|
||||
function highlight(str: string, lang: string) {
|
||||
switch (lang) {
|
||||
case "vue-template": {
|
||||
const indented = str
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((s) => ` ${s}`)
|
||||
.join("\n");
|
||||
return wrap(indented, "template");
|
||||
}
|
||||
case "vue-script":
|
||||
return wrap(str.trim(), "script");
|
||||
case "vue-style":
|
||||
return wrap(str.trim(), "style");
|
||||
default: {
|
||||
if (HLJS.getLanguage(lang) !== undefined) {
|
||||
return copyable(HLJS.highlight(str, { language: lang }).value);
|
||||
}
|
||||
|
||||
return copyable(markdown.utils.escapeHtml(str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wrap(str: string, tag: "template" | "script" | "style") {
|
||||
let openTag;
|
||||
let code;
|
||||
|
||||
switch (tag) {
|
||||
case "template":
|
||||
openTag = "<template>";
|
||||
code = HLJS.highlight(str, { language: "xml" }).value;
|
||||
break;
|
||||
case "script":
|
||||
openTag = '<script lang="ts" setup>';
|
||||
code = HLJS.highlight(str, { language: "typescript" }).value;
|
||||
break;
|
||||
case "style":
|
||||
openTag = '<style lang="postcss" scoped>';
|
||||
code = HLJS.highlight(str, { language: "scss" }).value;
|
||||
break;
|
||||
}
|
||||
|
||||
const openTagHtml = HLJS.highlight(openTag, { language: "xml" }).value;
|
||||
const closeTagHtml = HLJS.highlight(`</${tag}>`, { language: "xml" }).value;
|
||||
|
||||
return `${openTagHtml}${copyable(code)}${closeTagHtml}`;
|
||||
}
|
||||
|
||||
function copyable(code: string) {
|
||||
return `<div class="copyable">${code}</div>`;
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"property": "Property",
|
||||
"ram-usage": "RAM usage",
|
||||
"reboot": "Reboot",
|
||||
"reformat": "Reformat",
|
||||
"relative-time": {
|
||||
"day": "1 day | {n} days",
|
||||
"future": "In {str}",
|
||||
@@ -68,7 +67,6 @@
|
||||
"year": "1 year | {n} years"
|
||||
},
|
||||
"resume": "Resume",
|
||||
"save": "Save",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
"settings": "Settings",
|
||||
"shutdown": "Shutdown",
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
"property": "Propriété",
|
||||
"ram-usage": "Utilisation de la RAM",
|
||||
"reboot": "Redémarrer",
|
||||
"reformat": "Reformater",
|
||||
"relative-time": {
|
||||
"day": "1 jour | {n} jours",
|
||||
"future": "Dans {str}",
|
||||
@@ -68,7 +67,6 @@
|
||||
"year": "1 an | {n} ans"
|
||||
},
|
||||
"resume": "Reprendre",
|
||||
"save": "Enregistrer",
|
||||
"send-us-feedback": "Envoyez-nous vos commentaires",
|
||||
"settings": "Paramètres",
|
||||
"shutdown": "Arrêter",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"content-type": "^1.0.4",
|
||||
"cson-parser": "^4.0.7",
|
||||
"getopts": "^2.2.3",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "github:JsCommunity/http-request-plus#v1",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"pumpify": "^2.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/proxy",
|
||||
"version": "0.26.13",
|
||||
"version": "0.26.11",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "XO Proxy used to remotely execute backup jobs",
|
||||
"keywords": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"get-stream": "^6.0.0",
|
||||
"getopts": "^2.2.3",
|
||||
"golike-defer": "^0.5.1",
|
||||
"http-server-plus": "^1.0.0",
|
||||
"http-server-plus": "^0.12.0",
|
||||
"http2-proxy": "^5.0.53",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"jsonrpc-websocket-client": "^0.7.2",
|
||||
@@ -60,7 +60,7 @@
|
||||
"source-map-support": "^0.5.16",
|
||||
"stoppable": "^1.0.6",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.2.5",
|
||||
"xen-api": "^1.2.3",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "github:JsCommunity/http-request-plus#v1",
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pretty-ms": "^7.0.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xen-api": "^1.2.5"
|
||||
"xen-api": "^1.2.3"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
@@ -26,7 +26,7 @@
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "github:JsCommunity/http-request-plus#v1",
|
||||
"json-rpc-protocol": "^0.13.2",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,20 +1,6 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.79.2** (2023-02-20)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
### Bug fixes
|
||||
|
||||
- [Disk import] Fixes ` Cannot read properties of null (reading "length")` error
|
||||
- [Continuous Replication] Work-around _premature close_ error
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api 1.2.5
|
||||
- @xen-orchestra/proxy 0.26.13
|
||||
- xo-server 5.109.3
|
||||
|
||||
## **5.79.1** (2023-02-17)
|
||||
## **next**
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -29,13 +15,13 @@
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api 1.2.4
|
||||
- xen-api 1.2.3
|
||||
- @vates/nbd-client 1.0.1
|
||||
- @xen-orchestra/backups 0.29.6
|
||||
- @xen-orchestra/proxy 0.26.12
|
||||
- @xen-orchestra/proxy 0.26.11
|
||||
- xo-vmdk-to-vhd 2.5.3
|
||||
- xo-cli 0.14.4
|
||||
- xo-server 5.109.2
|
||||
- xo-server 5.109.1
|
||||
- xo-server-transport-email 0.6.1
|
||||
- xo-web 5.111.1
|
||||
|
||||
@@ -97,7 +83,7 @@
|
||||
|
||||
## **5.78.0** (2022-12-20)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -121,6 +107,8 @@
|
||||
|
||||
## **5.77.2** (2022-12-12)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backups] Fixes most of the _unexpected number of entries in backup cache_ errors
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- xo-cli minor
|
||||
- xen-api patch
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^1.2.5"
|
||||
"xen-api": "^1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xen-api",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.3",
|
||||
"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": "github:JsCommunity/http-request-plus#v1",
|
||||
"jest-diff": "^29.0.3",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"kindof": "^2.0.0",
|
||||
|
||||
@@ -90,7 +90,7 @@ export class Xapi extends EventEmitter {
|
||||
this._callTimeout = makeCallSetting(opts.callTimeout, 60 * 60 * 1e3) // 1 hour but will be reduced in the future
|
||||
this._httpInactivityTimeout = opts.httpInactivityTimeout ?? 5 * 60 * 1e3 // 5 mins
|
||||
this._eventPollDelay = opts.eventPollDelay ?? 60 * 1e3 // 1 min
|
||||
this._ignorePrematureClose = opts.ignorePrematureClose ?? true
|
||||
this._ignorePrematureClose = opts.ignorePrematureClose ?? false
|
||||
this._pool = null
|
||||
this._readOnly = Boolean(opts.readOnly)
|
||||
this._RecordsByType = { __proto__: null }
|
||||
@@ -563,9 +563,7 @@ export class Xapi extends EventEmitter {
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (this._ignorePrematureClose && error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||
console.warn(this._humanId, 'Xapi#putResource', pathname, error)
|
||||
} else {
|
||||
if (!(this._ignorePrematureClose && error.code === 'ERR_STREAM_PREMATURE_CLOSE')) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,47 +46,34 @@ async function connect() {
|
||||
return xo
|
||||
}
|
||||
|
||||
async function parseRegisterArgs(args, tokenDescription, acceptToken = false) {
|
||||
async function parseRegisterArgs(args) {
|
||||
const {
|
||||
allowUnauthorized,
|
||||
expiresIn,
|
||||
token,
|
||||
_: opts,
|
||||
} = getopts(args, {
|
||||
alias: {
|
||||
allowUnauthorized: 'au',
|
||||
token: 't',
|
||||
},
|
||||
boolean: ['allowUnauthorized'],
|
||||
stopEarly: true,
|
||||
string: ['expiresIn', 'token'],
|
||||
})
|
||||
|
||||
const result = {
|
||||
allowUnauthorized,
|
||||
expiresIn: expiresIn || undefined,
|
||||
url: opts[0],
|
||||
}
|
||||
|
||||
if (token !== '') {
|
||||
if (!acceptToken) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw '`token` option is not accepted by this command'
|
||||
}
|
||||
result.token = token
|
||||
} else {
|
||||
const [
|
||||
,
|
||||
_: [
|
||||
url,
|
||||
email,
|
||||
password = await new Promise(function (resolve) {
|
||||
process.stdout.write('Password: ')
|
||||
pw(resolve)
|
||||
}),
|
||||
] = opts
|
||||
result.token = await _createToken({ ...result, description: tokenDescription, email, password })
|
||||
}
|
||||
],
|
||||
} = getopts(args, {
|
||||
alias: {
|
||||
allowUnauthorized: 'au',
|
||||
},
|
||||
boolean: ['allowUnauthorized'],
|
||||
stopEarly: true,
|
||||
string: ['expiresIn'],
|
||||
})
|
||||
|
||||
return result
|
||||
return {
|
||||
allowUnauthorized,
|
||||
email,
|
||||
expiresIn: expiresIn || undefined,
|
||||
password,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
async function _createToken({ allowUnauthorized, description, email, expiresIn, password, url }) {
|
||||
@@ -206,20 +193,16 @@ const help = wrap(
|
||||
(function (pkg) {
|
||||
return `Usage:
|
||||
|
||||
$name --register [--allowUnauthorized] [--expiresIn <duration>] <XO-Server URL> <username> [<password>]
|
||||
$name --register [--allowUnauthorized] [--expiresIn <duration>] --token <token> <XO-Server URL>
|
||||
$name --register [--allowUnauthorized] [--expiresIn duration] <XO-Server URL> <username> [<password>]
|
||||
Registers the XO instance to use.
|
||||
|
||||
--allowUnauthorized, --au
|
||||
Accept invalid certificate (e.g. self-signed).
|
||||
|
||||
--expiresIn <duration>
|
||||
--expiresIn duration
|
||||
Can be used to change the validity duration of the
|
||||
authorization token (default: one month).
|
||||
|
||||
--token <token>
|
||||
An authentication token to use instead of username/password.
|
||||
|
||||
$name --createToken <params>…
|
||||
Create an authentication token for XO API.
|
||||
|
||||
@@ -311,8 +294,10 @@ async function main(args) {
|
||||
COMMANDS.help = help
|
||||
|
||||
async function createToken(args) {
|
||||
const { token } = await parseRegisterArgs(args, 'xo-cli --createToken')
|
||||
const opts = await parseRegisterArgs(args)
|
||||
opts.description = 'xo-cli --createToken'
|
||||
|
||||
const token = await _createToken(opts)
|
||||
console.warn('Authentication token created')
|
||||
console.warn()
|
||||
console.log(token)
|
||||
@@ -320,11 +305,13 @@ async function createToken(args) {
|
||||
COMMANDS.createToken = createToken
|
||||
|
||||
async function register(args) {
|
||||
const opts = await parseRegisterArgs(args, 'xo-cli --register', true)
|
||||
const opts = await parseRegisterArgs(args)
|
||||
opts.description = 'xo-cli --register'
|
||||
|
||||
await config.set({
|
||||
allowUnauthorized: opts.allowUnauthorized,
|
||||
server: opts.url,
|
||||
token: opts.token,
|
||||
token: await _createToken(opts),
|
||||
})
|
||||
}
|
||||
COMMANDS.register = register
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"chalk": "^5.0.1",
|
||||
"fs-extra": "^11.1.0",
|
||||
"getopts": "^2.3.0",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "github:JsCommunity/http-request-plus#v1",
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"micromatch": "^4.0.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.109.3",
|
||||
"version": "5.109.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -82,7 +82,7 @@
|
||||
"helmet": "^3.9.0",
|
||||
"highland": "^2.11.1",
|
||||
"http-proxy": "^1.16.2",
|
||||
"http-request-plus": "^1.0.0",
|
||||
"http-request-plus": "github:JsCommunity/http-request-plus#v1",
|
||||
"http-server-plus": "^1.0.0",
|
||||
"human-format": "^1.0.0",
|
||||
"iterable-backoff": "^0.1.0",
|
||||
@@ -131,7 +131,7 @@
|
||||
"vhd-lib": "^4.2.1",
|
||||
"ws": "^8.2.3",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.2.5",
|
||||
"xen-api": "^1.2.3",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.5.0",
|
||||
"xo-common": "^0.8.0",
|
||||
|
||||
@@ -232,7 +232,7 @@ async function handleImport(req, res, { type, name, description, vmdkData, srId,
|
||||
// didn't succeed to ensure the stream is completly consumed with resume/finished
|
||||
do {
|
||||
buffer = await readChunk(part, CHUNK_SIZE)
|
||||
} while (buffer?.length === CHUNK_SIZE)
|
||||
} while (buffer.length === CHUNK_SIZE)
|
||||
|
||||
res.end(format.response(0, vdi.$id))
|
||||
} catch (e) {
|
||||
|
||||
@@ -2598,8 +2598,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'al SR:',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} para importar',
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'VMs para importar',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Reiniciar',
|
||||
|
||||
@@ -2657,8 +2657,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'Sur le SR:',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} à importer',
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'VMs à importer',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Réinitialiser',
|
||||
|
||||
@@ -2276,7 +2276,7 @@ export default {
|
||||
// Original text: 'To SR:'
|
||||
vmImportToSr: undefined,
|
||||
|
||||
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
|
||||
// Original text: 'VMs to import'
|
||||
vmsToImport: undefined,
|
||||
|
||||
// Original text: 'Reset'
|
||||
|
||||
@@ -2490,7 +2490,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'Adattárolóra:',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'Importálandó VPS-el',
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -3746,8 +3746,8 @@ export default {
|
||||
// Original text: 'To SR:'
|
||||
vmImportToSr: 'Per SR:',
|
||||
|
||||
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} da importare',
|
||||
// Original text: 'VMs to import'
|
||||
vmsToImport: 'VMs da importare',
|
||||
|
||||
// Original text: 'Reset'
|
||||
importVmsCleanList: 'Ripristina',
|
||||
|
||||
@@ -2280,8 +2280,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'To SR:',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'VMs to import',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Reset',
|
||||
|
||||
@@ -2279,8 +2279,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'Enviar para SR:',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'Importar VM{nVms, plural, one {} other {s}} ',
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'Importar VMs',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Reiniciar',
|
||||
|
||||
@@ -2600,7 +2600,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'В SR:',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'ВМ для импорта',
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -3223,7 +3223,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'SR:',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: "içe aktarılacak VM'ler",
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -1739,7 +1739,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: '到存储库',
|
||||
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: '导入虚拟机',
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -9,10 +9,7 @@ const messages = {
|
||||
description: 'Description',
|
||||
deleteSourceVm: 'Delete source VM',
|
||||
expiration: 'Expiration',
|
||||
hostIp: 'Host IP',
|
||||
keyValue: '{key}: {value}',
|
||||
sslCertificate: 'SSL certificate',
|
||||
vmSrUsage: '{used} used of {total} ({free} free)',
|
||||
|
||||
notDefined: 'Not defined',
|
||||
statusConnecting: 'Connecting',
|
||||
@@ -1612,13 +1609,12 @@ const messages = {
|
||||
// ---- VM import ---
|
||||
fileType: 'File type:',
|
||||
fromUrl: 'From URL',
|
||||
fromVmware: 'From VMware',
|
||||
importVmsList: 'Drop OVA or XVA files here to import Virtual Machines.',
|
||||
noSelectedVms: 'No selected VMs.',
|
||||
url: 'URL:',
|
||||
vmImportToPool: 'To Pool:',
|
||||
vmImportToSr: 'To SR:',
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
|
||||
vmsToImport: 'VMs to import',
|
||||
importVmsCleanList: 'Reset',
|
||||
vmImportSuccess: 'VM import success',
|
||||
vmImportFailed: 'VM import failed',
|
||||
|
||||
@@ -3468,10 +3468,3 @@ export const synchronizeNetbox = pools =>
|
||||
body: _('syncNetboxWarning'),
|
||||
icon: 'refresh',
|
||||
}).then(() => _call('netbox.synchronize', { pools: resolveIds(pools) }))
|
||||
|
||||
// ESXi import ---------------------------------------------------------------
|
||||
|
||||
export const esxiConnect = (host, user, password, sslVerify) =>
|
||||
_call('esxi.connect', { host, user, password, sslVerify })
|
||||
|
||||
export const importVmFromEsxi = params => _call('vm.importFromEsxi', params)
|
||||
|
||||
@@ -7,7 +7,6 @@ import { NavLink, NavTabs } from 'nav'
|
||||
import { routes } from 'utils'
|
||||
|
||||
import DiskImport from '../disk-import'
|
||||
import EsxiImport from '../vm-import/esxi-import'
|
||||
import VmImport from '../vm-import'
|
||||
|
||||
const HEADER = (
|
||||
@@ -26,9 +25,6 @@ const HEADER = (
|
||||
<NavLink to='/import/disk'>
|
||||
<Icon icon='disk' /> {_('labelDisk')}
|
||||
</NavLink>
|
||||
<NavLink to='/import/vmware'>
|
||||
<Icon icon='vm' /> {_('fromVmware')}
|
||||
</NavLink>
|
||||
</NavTabs>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -38,7 +34,6 @@ const HEADER = (
|
||||
const Import = routes('vm', {
|
||||
disk: DiskImport,
|
||||
vm: VmImport,
|
||||
vmware: EsxiImport,
|
||||
})(({ children }) => (
|
||||
<Page header={HEADER} title='newImport' formatTitle>
|
||||
{children}
|
||||
|
||||
@@ -494,11 +494,6 @@ export default class Menu extends Component {
|
||||
icon: 'disk',
|
||||
label: 'labelDisk',
|
||||
},
|
||||
{
|
||||
to: '/import/vmware',
|
||||
icon: 'vm',
|
||||
label: 'fromVmware',
|
||||
},
|
||||
],
|
||||
},
|
||||
!(noOperatablePools && noResourceSets) && {
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { esxiConnect, importVmFromEsxi, isSrWritable } from 'xo'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||
import { isEmpty, map } from 'lodash'
|
||||
import { linkState } from 'reaclette-utils'
|
||||
import { Password, Select } from 'form'
|
||||
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import VmData from './vm-data'
|
||||
|
||||
const getInitialState = () => ({
|
||||
hasCertificate: true,
|
||||
hostIp: '',
|
||||
isConnected: false,
|
||||
network: undefined,
|
||||
password: '',
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
user: '',
|
||||
vm: undefined,
|
||||
vmData: undefined,
|
||||
vmsById: undefined,
|
||||
})
|
||||
|
||||
const EsxiImport = decorate([
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
importVm:
|
||||
() =>
|
||||
({ hasCertificate, hostIp, network, password, sr, user, vmData }) => {
|
||||
importVmFromEsxi({
|
||||
host: hostIp,
|
||||
network: network.id,
|
||||
password,
|
||||
sr,
|
||||
sslVerify: hasCertificate,
|
||||
user,
|
||||
vm: vmData.id,
|
||||
})
|
||||
},
|
||||
connect:
|
||||
() =>
|
||||
async ({ hostIp, hasCertificate, password, user }) => {
|
||||
const vms = await esxiConnect(hostIp, user, password, hasCertificate)
|
||||
return { isConnected: true, vmsById: vms.reduce((vms, vm) => ({ ...vms, [vm.id]: vm }), {}) }
|
||||
},
|
||||
linkState,
|
||||
onChangeVm: (_, vm) => state => ({ vm, vmData: state.vmsById[vm.value] }),
|
||||
onChangeVmData: (_, vmData) => ({ vmData }),
|
||||
onChangeNetwork: (_, network) => ({ network }),
|
||||
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||
onChangeSr: (_, sr) => ({ sr }),
|
||||
toggleCertificateCheck:
|
||||
(_, { target: { checked, name } }) =>
|
||||
state => ({
|
||||
...state,
|
||||
[name]: checked,
|
||||
}),
|
||||
reset: getInitialState,
|
||||
},
|
||||
computed: {
|
||||
selectVmOptions: ({ vmsById }) =>
|
||||
map(vmsById, vm => ({
|
||||
label: vm.nameLabel,
|
||||
value: vm.id,
|
||||
})),
|
||||
networkpredicate:
|
||||
({ pool }) =>
|
||||
network =>
|
||||
network.$poolId === pool?.uuid,
|
||||
srPredicate:
|
||||
({ pool }) =>
|
||||
sr =>
|
||||
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||
},
|
||||
}),
|
||||
injectIntl,
|
||||
injectState,
|
||||
({
|
||||
effects: {
|
||||
connect,
|
||||
importVm,
|
||||
linkState,
|
||||
onChangeVm,
|
||||
onChangeVmData,
|
||||
onChangeNetwork,
|
||||
onChangePool,
|
||||
onChangeSr,
|
||||
reset,
|
||||
srPredicate,
|
||||
toggleCertificateCheck,
|
||||
},
|
||||
intl: { formatMessage },
|
||||
state: {
|
||||
hasCertificate,
|
||||
hostIp,
|
||||
isConnected,
|
||||
network,
|
||||
networkPredicate,
|
||||
password,
|
||||
pool,
|
||||
selectVmOptions,
|
||||
sr,
|
||||
user,
|
||||
vm,
|
||||
vmsById,
|
||||
vmData,
|
||||
},
|
||||
}) => (
|
||||
<div>
|
||||
{!isConnected && (
|
||||
<form id='esxi-connect-form'>
|
||||
<Row>
|
||||
<LabelCol>{_('hostIp')}</LabelCol>
|
||||
<InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
name='hostIp'
|
||||
onChange={linkState}
|
||||
placeholder='192.168.2.20'
|
||||
required
|
||||
value={hostIp}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('user')}</LabelCol>
|
||||
<InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
name='user'
|
||||
onChange={linkState}
|
||||
placeholder={formatMessage(messages.user)}
|
||||
required
|
||||
value={user}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('password')}</LabelCol>
|
||||
<InputCol>
|
||||
<Password
|
||||
name='password'
|
||||
onChange={linkState}
|
||||
placeholder={formatMessage(messages.password)}
|
||||
required
|
||||
value={password}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('sslCertificate')}</LabelCol>
|
||||
<InputCol>
|
||||
<input
|
||||
checked={hasCertificate}
|
||||
name='hasCertificate'
|
||||
onChange={toggleCertificateCheck}
|
||||
type='checkbox'
|
||||
value={hasCertificate}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
form='esxi-connect-form'
|
||||
handler={connect}
|
||||
icon='connect'
|
||||
type='submit'
|
||||
>
|
||||
{_('serverConnect')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('formReset')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{isConnected && (
|
||||
<form id='esxi-migrate-form'>
|
||||
<Row>
|
||||
<LabelCol>{_('vm')}</LabelCol>
|
||||
<InputCol>
|
||||
<Select disabled={isEmpty(vmsById)} onChange={onChangeVm} options={selectVmOptions} required value={vm} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectPool onChange={onChangePool} required value={pool} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectSr
|
||||
disabled={pool === undefined}
|
||||
onChange={onChangeSr}
|
||||
predicate={srPredicate}
|
||||
required
|
||||
value={sr}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('network')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectNetwork
|
||||
disabled={pool === undefined}
|
||||
onChange={onChangeNetwork}
|
||||
predicate={networkPredicate}
|
||||
required
|
||||
value={network}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
{vm !== undefined && (
|
||||
<div>
|
||||
<hr />
|
||||
<h5>{_('vmsToImport', { nVms: 1 })}</h5>
|
||||
<VmData data={vmData} onChange={onChangeVmData} pool={pool} />
|
||||
</div>
|
||||
)}
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
disabled={vm === undefined}
|
||||
form='esxi-migrate-form'
|
||||
handler={importVm}
|
||||
icon='import'
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('formReset')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default EsxiImport
|
||||
@@ -1,29 +1,302 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import * as FormGrid from 'form-grid'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import Dropzone from 'dropzone'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { Container } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { importVm, importVms, isSrWritable } from 'xo'
|
||||
import { Select, SizeInput, Toggle } from 'form'
|
||||
import { createFinder, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { connectStore, formatSize, mapPlus, noop } from 'utils'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
|
||||
import XvaImport from './xva-import'
|
||||
import UrlImport from './url-import'
|
||||
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import parseOvaFile from './ova'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const RENDER_BY_TYPE = {
|
||||
xva: <XvaImport />,
|
||||
url: <UrlImport />,
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
]
|
||||
|
||||
const FORMAT_TO_HANDLER = {
|
||||
ova: parseOvaFile,
|
||||
xva: noop,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore(
|
||||
() => {
|
||||
const getHostMaster = createGetObject((_, props) => props.pool.master)
|
||||
const getPifs = createGetObjectsOfType('PIF').pick((state, props) => getHostMaster(state, props).$PIFs)
|
||||
const getDefaultNetworkId = createSelector(createFinder(getPifs, [pif => pif.management]), pif => pif.$network)
|
||||
|
||||
return {
|
||||
defaultNetwork: getDefaultNetworkId,
|
||||
}
|
||||
},
|
||||
{ withRef: true }
|
||||
)
|
||||
class VmData extends Component {
|
||||
static propTypes = {
|
||||
descriptionLabel: PropTypes.string,
|
||||
disks: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
capacity: PropTypes.number.isRequired,
|
||||
descriptionLabel: PropTypes.string.isRequired,
|
||||
nameLabel: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
compression: PropTypes.string,
|
||||
})
|
||||
),
|
||||
memory: PropTypes.number,
|
||||
nameLabel: PropTypes.string,
|
||||
nCpus: PropTypes.number,
|
||||
networks: PropTypes.array,
|
||||
pool: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
get value() {
|
||||
const { props, refs } = this
|
||||
return {
|
||||
descriptionLabel: refs.descriptionLabel.value,
|
||||
disks: map(props.disks, ({ capacity, path, compression, position }, diskId) => ({
|
||||
capacity,
|
||||
descriptionLabel: refs[`disk-description-${diskId}`].value,
|
||||
nameLabel: refs[`disk-name-${diskId}`].value,
|
||||
path,
|
||||
position,
|
||||
compression,
|
||||
})),
|
||||
memory: +refs.memory.value,
|
||||
nameLabel: refs.nameLabel.value,
|
||||
networks: map(props.networks, (_, networkId) => {
|
||||
const network = refs[`network-${networkId}`].value
|
||||
return network.id ? network.id : network
|
||||
}),
|
||||
nCpus: +refs.nCpus.value,
|
||||
tables: props.tables,
|
||||
}
|
||||
}
|
||||
|
||||
_getNetworkPredicate = createSelector(
|
||||
() => this.props.pool.id,
|
||||
id => network => network.$pool === id
|
||||
)
|
||||
|
||||
render() {
|
||||
const { descriptionLabel, defaultNetwork, disks, memory, nameLabel, nCpus, networks } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameLabel')}</label>
|
||||
<input className='form-control' ref='nameLabel' defaultValue={nameLabel} type='text' required />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameDescription')}</label>
|
||||
<input className='form-control' ref='descriptionLabel' defaultValue={descriptionLabel} type='text' />
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('nCpus')}</label>
|
||||
<input className='form-control' ref='nCpus' defaultValue={nCpus} type='number' required />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmMemory')}</label>
|
||||
<SizeInput defaultValue={memory} ref='memory' required />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
{!isEmpty(disks)
|
||||
? map(disks, (disk, diskId) => (
|
||||
<Row key={diskId}>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
{_('diskInfo', {
|
||||
position: `${disk.position}`,
|
||||
capacity: formatSize(disk.capacity),
|
||||
})}
|
||||
</label>
|
||||
<input
|
||||
className='form-control'
|
||||
ref={`disk-name-${diskId}`}
|
||||
defaultValue={disk.nameLabel}
|
||||
type='text'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('diskDescription')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
ref={`disk-description-${diskId}`}
|
||||
defaultValue={disk.descriptionLabel}
|
||||
type='text'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
))
|
||||
: _('noDisks')}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{networks.length > 0
|
||||
? map(networks, (name, networkId) => (
|
||||
<div className='form-group' key={networkId}>
|
||||
<label>{_('networkInfo', { name })}</label>
|
||||
<SelectNetwork
|
||||
defaultValue={defaultNetwork}
|
||||
ref={`network-${networkId}`}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: _('noNetworks')}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const parseFile = async (file, type, func) => {
|
||||
try {
|
||||
return {
|
||||
data: await func(file),
|
||||
file,
|
||||
type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { error, file, type }
|
||||
}
|
||||
}
|
||||
|
||||
const getRedirectionUrl = vms =>
|
||||
vms.length === 0
|
||||
? undefined // no redirect
|
||||
: vms.length === 1
|
||||
? `/vms/${vms[0]}`
|
||||
: `/home?s=${encodeURIComponent(new CM.Property('id', new CM.Or(vms.map(_ => new CM.String(_)))).toString())}&t=VM`
|
||||
|
||||
export default class Import extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isFromUrl: false,
|
||||
type: {
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
url: '',
|
||||
vms: [],
|
||||
}
|
||||
}
|
||||
|
||||
_import = () => {
|
||||
const { state } = this
|
||||
return importVms(
|
||||
mapPlus(state.vms, (vm, push, vmIndex) => {
|
||||
if (!vm.error) {
|
||||
const ref = this.refs[`vm-data-${vmIndex}`]
|
||||
push({
|
||||
...vm,
|
||||
data: ref && ref.value,
|
||||
})
|
||||
}
|
||||
}),
|
||||
state.sr
|
||||
)
|
||||
}
|
||||
|
||||
_importVmFromUrl = () => {
|
||||
const { type, url } = this.state
|
||||
const file = {
|
||||
name: decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)),
|
||||
}
|
||||
return importVm(file, type.value, undefined, this.state.sr, url)
|
||||
}
|
||||
|
||||
_handleDrop = async files => {
|
||||
this.setState({
|
||||
vms: [],
|
||||
})
|
||||
|
||||
const vms = await Promise.all(
|
||||
mapPlus(files, (file, push) => {
|
||||
const { name } = file
|
||||
const extIndex = name.lastIndexOf('.')
|
||||
|
||||
let func
|
||||
let type
|
||||
|
||||
if (extIndex >= 0 && (type = name.slice(extIndex + 1).toLowerCase()) && (func = FORMAT_TO_HANDLER[type])) {
|
||||
push(parseFile(file, type, func))
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
this.setState({
|
||||
vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),
|
||||
})
|
||||
}
|
||||
|
||||
_handleCleanSelectedVms = () => {
|
||||
this.setState({
|
||||
vms: [],
|
||||
})
|
||||
}
|
||||
|
||||
_handleSelectedPool = pool => {
|
||||
if (pool === '') {
|
||||
this.setState({
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
srPredicate: undefined,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
pool,
|
||||
sr: pool.default_SR,
|
||||
srPredicate: sr => sr.$pool === this.state.pool.id && isSrWritable(sr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_handleSelectedSr = sr => {
|
||||
this.setState({
|
||||
sr: sr === '' ? undefined : sr,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isFromUrl } = this.state
|
||||
const { isFromUrl, pool, sr, srPredicate, type, url, vms } = this.state
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -31,7 +304,113 @@ export default class Import extends Component {
|
||||
<p>
|
||||
<Toggle value={isFromUrl} onChange={this.toggleState('isFromUrl')} /> {_('fromUrl')}
|
||||
</p>
|
||||
{RENDER_BY_TYPE[isFromUrl ? 'url' : 'xva']}
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectPool value={pool} onChange={this._handleSelectedPool} required />
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectSr
|
||||
disabled={!pool}
|
||||
onChange={this._handleSelectedSr}
|
||||
predicate={srPredicate}
|
||||
required
|
||||
value={sr}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
{sr &&
|
||||
(!isFromUrl ? (
|
||||
<div>
|
||||
<Dropzone onDrop={this._handleDrop} message={_('importVmsList')} />
|
||||
<hr />
|
||||
<h5>{_('vmsToImport')}</h5>
|
||||
{vms.length > 0 ? (
|
||||
<div>
|
||||
{map(vms, ({ data, error, file, type }, vmIndex) => (
|
||||
<div key={file.preview} className={styles.vmContainer}>
|
||||
<strong>{file.name}</strong>
|
||||
<span className='pull-right'>
|
||||
<strong>{`(${formatSize(file.size)})`}</strong>
|
||||
</span>
|
||||
{!error ? (
|
||||
data && (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-info' role='alert'>
|
||||
<strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}
|
||||
</div>
|
||||
<VmData {...data} ref={`vm-data-${vmIndex}`} pool={pool} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
<strong>{_('vmImportError')}</strong>{' '}
|
||||
{(error && error.message) || _('noVmImportErrorDescription')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{_('noSelectedVms')}</p>
|
||||
)}
|
||||
<hr />
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!vms.length}
|
||||
className='mr-1'
|
||||
form='import-form'
|
||||
handler={this._import}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={this._handleCleanSelectedVms}>{_('importVmsCleanList')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('url')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
onChange={this.linkState('url')}
|
||||
placeholder='https://my-company.net/vm.xva'
|
||||
type='url'
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('fileType')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Select onChange={this.linkState('type')} options={FILE_TYPES} required value={type} />
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1 mt-1'
|
||||
disabled={isEmpty(url)}
|
||||
form='import-form'
|
||||
handler={this._importVmFromUrl}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
))}
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { importVm, isSrWritable } from 'xo'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { linkState } from 'reaclette-utils'
|
||||
import { Select } from 'form'
|
||||
import { SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import { getRedirectionUrl } from './utils'
|
||||
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
]
|
||||
|
||||
const getInitialState = () => ({
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
type: {
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
url: '',
|
||||
})
|
||||
|
||||
const UrlImport = decorate([
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
handleImport() {
|
||||
const { type, url } = this.state
|
||||
const file = {
|
||||
name: decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)),
|
||||
}
|
||||
importVm(file, type.value, undefined, this.state.sr, url)
|
||||
},
|
||||
linkState,
|
||||
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||
onChangeSr: (_, sr) => ({ sr }),
|
||||
reset: getInitialState,
|
||||
},
|
||||
computed: {
|
||||
srPredicate:
|
||||
({ pool }) =>
|
||||
sr =>
|
||||
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects: { handleImport, linkState, onChangePool, onChangeSr, reset },
|
||||
state: { pool, sr, srPredicate, type, url },
|
||||
}) => (
|
||||
<div>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectPool value={pool} onChange={onChangePool} required />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectSr disabled={pool === undefined} onChange={onChangeSr} predicate={srPredicate} required value={sr} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('url')}</LabelCol>
|
||||
<InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
name='url'
|
||||
onChange={linkState}
|
||||
placeholder='https://my-company.net/vm.xva'
|
||||
type='url'
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('fileType')}</LabelCol>
|
||||
<InputCol>
|
||||
<Select name='type' onChange={linkState} options={FILE_TYPES} required value={type} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
disabled={isEmpty(url)}
|
||||
form='import-form'
|
||||
handler={handleImport}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('formReset')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default UrlImport
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
|
||||
const getRedirectionUrl = vms =>
|
||||
vms.length === 0
|
||||
? undefined // no redirect
|
||||
: vms.length === 1
|
||||
? `/vms/${vms[0]}`
|
||||
: `/home?s=${encodeURIComponent(new CM.Property('id', new CM.Or(vms.map(_ => new CM.String(_)))).toString())}&t=VM`
|
||||
@@ -1,187 +0,0 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { Col, Row } from 'grid'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { createFinder, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { map } from 'lodash'
|
||||
import { SelectNetwork } from 'select-objects'
|
||||
import { SizeInput } from 'form'
|
||||
|
||||
const VmData = decorate([
|
||||
connectStore(() => {
|
||||
const getHostMaster = createGetObject((_, props) => props.pool.master)
|
||||
const getPifs = createGetObjectsOfType('PIF').pick((state, props) => getHostMaster(state, props).$PIFs)
|
||||
const getDefaultNetworkId = createSelector(createFinder(getPifs, [pif => pif.management]), pif => pif.$network)
|
||||
|
||||
return {
|
||||
defaultNetwork: getDefaultNetworkId,
|
||||
}
|
||||
}),
|
||||
provideState({
|
||||
effects: {
|
||||
onChangeDisks(_, { target: { name, value } }) {
|
||||
const { onChange, data: prevValue } = this.props
|
||||
// name: nameLabel-index or descriptionLabel-index
|
||||
const data = name.split('-')
|
||||
const index = data[1]
|
||||
onChange({
|
||||
...prevValue,
|
||||
disks: {
|
||||
...prevValue.disks,
|
||||
[index]: { ...prevValue.disks[index], [data[0]]: value },
|
||||
},
|
||||
})
|
||||
},
|
||||
onChangeMemory(_, memory) {
|
||||
const { onChange, data: prevData } = this.props
|
||||
onChange({
|
||||
...prevData,
|
||||
memory,
|
||||
})
|
||||
},
|
||||
onChangeNCpus(_, { target: { value } }) {
|
||||
const { onChange, data: prevData } = this.props
|
||||
onChange({
|
||||
...prevData,
|
||||
nCpus: +value,
|
||||
})
|
||||
},
|
||||
onChangeNetworks(_, network, networkIndex) {
|
||||
const { onChange, data } = this.props
|
||||
onChange({
|
||||
...data,
|
||||
networks: data.networks.map((prevNetwork, index) => (index === networkIndex ? network.id : prevNetwork)),
|
||||
})
|
||||
},
|
||||
onChangeValue(_, { target: { name, value } }) {
|
||||
const { onChange, data } = this.props
|
||||
onChange({
|
||||
...data,
|
||||
[name]: value,
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
networkPredicate:
|
||||
(_, { pool }) =>
|
||||
network =>
|
||||
pool.id === network.$pool,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
data,
|
||||
defaultNetwork,
|
||||
effects: { onChangeDisks, onChangeMemory, onChangeNCpus, onChangeNetworks, onChangeValue },
|
||||
state: { networkPredicate },
|
||||
}) => (
|
||||
<div>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameLabel')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='nameLabel'
|
||||
onChange={onChangeValue}
|
||||
type='text'
|
||||
required
|
||||
value={data.nameLabel}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameDescription')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='descriptionLabel'
|
||||
onChange={onChangeValue}
|
||||
type='text'
|
||||
value={data.descriptionLabel}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('nCpus')}</label>
|
||||
<input className='form-control' onChange={onChangeNCpus} type='number' required value={data.nCpus} />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmMemory')}</label>
|
||||
<SizeInput onChange={onChangeMemory} required value={data.memory} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
{map(data.disks, (disk, diskId) => (
|
||||
<Row key={diskId}>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
{_('diskInfo', {
|
||||
position: `${disk.position}`,
|
||||
capacity: formatSize(disk.capacity),
|
||||
})}
|
||||
</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name={`nameLabel-${diskId}`}
|
||||
onChange={onChangeDisks}
|
||||
type='text'
|
||||
required
|
||||
value={disk.nameLabel}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('diskDescription')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name={`descriptionLabel-${diskId}`}
|
||||
onChange={onChangeDisks}
|
||||
type='text'
|
||||
required
|
||||
value={disk.descriptionLabel}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{map(data.networks, (networkId, index) => (
|
||||
<div className='form-group' key={networkId}>
|
||||
<label>{_('networkInfo', { name: index + 1 })}</label>
|
||||
<SelectNetwork
|
||||
onChange={network => onChangeNetworks(network, index)}
|
||||
predicate={networkPredicate}
|
||||
value={data.networks[index] ?? defaultNetwork}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
{data.storage !== undefined && (
|
||||
<Row className='mt-1'>
|
||||
<Col mediumSize={12}>
|
||||
<div className='form-group'>
|
||||
<label>{_('homeSrPage')}:</label>{' '}
|
||||
<span>
|
||||
{_('vmSrUsage', {
|
||||
free: formatSize(data.storage.free),
|
||||
total: formatSize(data.storage.used + data.storage.free),
|
||||
used: formatSize(data.storage.used),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default VmData
|
||||
@@ -1,202 +0,0 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import decorate from 'apply-decorators'
|
||||
import Dropzone from 'dropzone'
|
||||
import React from 'react'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { connectStore, formatSize, mapPlus, noop } from 'utils'
|
||||
import { importVms, isSrWritable } from 'xo'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||
import { orderBy } from 'lodash'
|
||||
import { SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import parseOvaFile from './ova'
|
||||
import styles from './index.css'
|
||||
import VmData from './vm-data'
|
||||
import { getRedirectionUrl } from './utils'
|
||||
|
||||
const FORMAT_TO_HANDLER = {
|
||||
ova: parseOvaFile,
|
||||
xva: noop,
|
||||
}
|
||||
|
||||
const parseFile = async (file, type, func) => {
|
||||
try {
|
||||
return {
|
||||
data: await func(file),
|
||||
file,
|
||||
type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { error, file, type }
|
||||
}
|
||||
}
|
||||
|
||||
const getInitialState = () => ({
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
vms: [],
|
||||
})
|
||||
|
||||
const XvaImport = decorate([
|
||||
connectStore(() => ({
|
||||
networksByName: createGetObjectsOfType('network').groupBy('name_label'),
|
||||
})),
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
handleImport:
|
||||
() =>
|
||||
({ sr, vms }) => {
|
||||
importVms(
|
||||
mapPlus(vms, (vm, push) => {
|
||||
if (!vm.error) {
|
||||
const { data } = vm
|
||||
push(
|
||||
data === undefined
|
||||
? { ...vm }
|
||||
: {
|
||||
...vm,
|
||||
data: {
|
||||
...vm.data,
|
||||
disks: Object.values(vm.data.disks),
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}),
|
||||
sr
|
||||
)
|
||||
},
|
||||
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||
onChangeSr: (_, sr) => ({ sr }),
|
||||
onChangeVmData: (_, data, vmIndex) => state => {
|
||||
const vms = [...state.vms]
|
||||
vms[vmIndex].data = data
|
||||
return { vms }
|
||||
},
|
||||
onDrop: (_, files) => async (_, props) => {
|
||||
const vms = (
|
||||
await Promise.all(
|
||||
mapPlus(files, (file, push) => {
|
||||
const { name } = file
|
||||
const extIndex = name.lastIndexOf('.')
|
||||
|
||||
let func
|
||||
let type
|
||||
|
||||
if (
|
||||
extIndex >= 0 &&
|
||||
(type = name.slice(extIndex + 1).toLowerCase()) &&
|
||||
(func = FORMAT_TO_HANDLER[type])
|
||||
) {
|
||||
push(parseFile(file, type, func))
|
||||
}
|
||||
})
|
||||
)
|
||||
).map(vm => {
|
||||
const { data } = vm
|
||||
return data === undefined
|
||||
? vm
|
||||
: {
|
||||
...vm,
|
||||
data: {
|
||||
...data,
|
||||
networks: data.networks.map(name => props.networksByName[name][0].id),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),
|
||||
}
|
||||
},
|
||||
reset: getInitialState,
|
||||
},
|
||||
computed: {
|
||||
srPredicate:
|
||||
({ pool }) =>
|
||||
sr =>
|
||||
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects: { handleImport, onChangePool, onChangeSr, onChangeVmData, onDrop, reset },
|
||||
state: { pool, sr, srPredicate, vms },
|
||||
}) => (
|
||||
<div>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectPool value={pool} onChange={onChangePool} required />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectSr disabled={pool === undefined} onChange={onChangeSr} predicate={srPredicate} required value={sr} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<div>
|
||||
<Dropzone onDrop={onDrop} message={_('importVmsList')} />
|
||||
<hr />
|
||||
<h5>{_('vmsToImport', { nVms: vms.length })}</h5>
|
||||
{vms.length > 0 ? (
|
||||
<div>
|
||||
{vms.map(({ data, error, file, type }, vmIndex) => (
|
||||
<div key={file.preview} className={styles.vmContainer}>
|
||||
<strong>{file.name}</strong>
|
||||
<span className='pull-right'>
|
||||
<strong>{`(${formatSize(file.size)})`}</strong>
|
||||
</span>
|
||||
{!error ? (
|
||||
data && (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-info' role='alert'>
|
||||
<strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}
|
||||
</div>
|
||||
<VmData data={data} onChange={data => onChangeVmData(data, vmIndex)} pool={pool} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
<strong>{_('vmImportError')}</strong>{' '}
|
||||
{(error && error.message) || _('noVmImportErrorDescription')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{_('noSelectedVms')}</p>
|
||||
)}
|
||||
<hr />
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={vms.length === 0}
|
||||
className='mr-1'
|
||||
form='import-form'
|
||||
handler={handleImport}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('importVmsCleanList')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default XvaImport
|
||||
Reference in New Issue
Block a user