mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-02-04 15:01:46 +00:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
parent
f8408469f7
commit
4f2688f6ab
212
pkg/harvester/components/DiskTags.vue
Normal file
212
pkg/harvester/components/DiskTags.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<script>
|
||||||
|
import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
|
import Tag from '@shell/components/Tag';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Tags',
|
||||||
|
|
||||||
|
components: { Tag },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
labelKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
addLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
addLabelKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
canAdd: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _CREATE,
|
||||||
|
},
|
||||||
|
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tags: this.value,
|
||||||
|
inputVisible: false,
|
||||||
|
inputValue: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isCreate() {
|
||||||
|
return this.mode === _CREATE;
|
||||||
|
},
|
||||||
|
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
isEdit() {
|
||||||
|
return this.mode === _EDIT;
|
||||||
|
},
|
||||||
|
|
||||||
|
canRemove() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
|
||||||
|
addVisible() {
|
||||||
|
return this.canAdd && !this.isView;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onClickPlusButton() {
|
||||||
|
this.inputVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if ( this.$refs.addTagInput ) {
|
||||||
|
this.$refs.addTagInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmAdd() {
|
||||||
|
if (this.inputValue && !this.value.includes(this.inputValue)) {
|
||||||
|
this.tags.push(this.inputValue);
|
||||||
|
this.$emit('input', this.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputValue = '';
|
||||||
|
this.inputVisible = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemoveTag(tag) {
|
||||||
|
this.tags = this.tags.filter(v => v !== tag);
|
||||||
|
this.$emit('input', this.tags);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="label">
|
||||||
|
<div class="text-label">
|
||||||
|
<t
|
||||||
|
v-if="labelKey"
|
||||||
|
:k="labelKey"
|
||||||
|
/>
|
||||||
|
<template v-else-if="label">
|
||||||
|
{{ label }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-10">
|
||||||
|
<Tag
|
||||||
|
v-for="(tag) in value"
|
||||||
|
:key="tag"
|
||||||
|
class="tag"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
v-if="canRemove"
|
||||||
|
class="icon icon-close ml-5 icon-sm"
|
||||||
|
@click="(e) => onRemoveTag(tag)"
|
||||||
|
/>
|
||||||
|
</Tag>
|
||||||
|
<span
|
||||||
|
v-if="addVisible && !inputVisible"
|
||||||
|
class="tag add"
|
||||||
|
@click="onClickPlusButton"
|
||||||
|
>
|
||||||
|
<i class="icon icon-plus icon-sm" />
|
||||||
|
<span>
|
||||||
|
<t
|
||||||
|
v-if="addLabelKey"
|
||||||
|
:k="addLabelKey"
|
||||||
|
/>
|
||||||
|
<template v-else-if="addLabel">
|
||||||
|
{{ addLabel }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="addVisible && inputVisible"
|
||||||
|
class="tag input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="addTagInput"
|
||||||
|
v-model="inputValue"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@blur="confirmAdd"
|
||||||
|
@keydown.enter.prevent="confirmAdd"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag {
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
list-style: none;
|
||||||
|
display: inline-block;
|
||||||
|
height: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
padding-inline: 7px;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--accent-btn);
|
||||||
|
opacity: 1;
|
||||||
|
text-align: start;
|
||||||
|
color: var(--link);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.add {
|
||||||
|
background: var(--body-bg);
|
||||||
|
border-style: dashed;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.input {
|
||||||
|
border: none;
|
||||||
|
border-radius: none;
|
||||||
|
background: var(--body-bg);
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
134
pkg/harvester/components/FilterBySriov.vue
Normal file
134
pkg/harvester/components/FilterBySriov.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterFilterLabel',
|
||||||
|
|
||||||
|
components: { LabeledSelect },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
rows: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
parentSriovOptions: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
parentSriovLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { parentSriov: this.$route.query?.parentSriov || null };
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
remove() {
|
||||||
|
this.parentSriov = null;
|
||||||
|
this.filterRows();
|
||||||
|
},
|
||||||
|
|
||||||
|
filterRows() {
|
||||||
|
const rows = this.rows.filter((row) => {
|
||||||
|
if (!this.parentSriov) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = row.labels[this.parentSriovLabel];
|
||||||
|
|
||||||
|
return label === this.parentSriov;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$emit('change-rows', rows, this.parentSriov);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
parentSriov: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.filterRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="filter">
|
||||||
|
<span v-if="parentSriov" class="banner-item bg-warning">
|
||||||
|
{{ parentSriov }} <i class="icon icon-close" @click="remove()" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<v-popover
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom-end"
|
||||||
|
>
|
||||||
|
<slot name="header">
|
||||||
|
<button ref="actionDropDown" class="btn bg-primary mr-10">
|
||||||
|
<slot name="title">
|
||||||
|
{{ label }}
|
||||||
|
</slot>
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<template v-slot:popover>
|
||||||
|
<div class="filter-popup">
|
||||||
|
<div>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="parentSriov"
|
||||||
|
:options="parentSriovOptions"
|
||||||
|
:searchable="true"
|
||||||
|
:label="label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.banner-item {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter-popup {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .box {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40% 40% 10%;
|
||||||
|
column-gap: 1.75%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
247
pkg/harvester/components/FilterLabel.vue
Normal file
247
pkg/harvester/components/FilterLabel.vue
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<script>
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import Select from '@shell/components/form/Select';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterFilterLabel',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Select,
|
||||||
|
ArrayList,
|
||||||
|
LabeledInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
rows: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchLabels: [],
|
||||||
|
defaultAddValue: {
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
optionLabels() {
|
||||||
|
const labels = this.rows.map((row) => {
|
||||||
|
return Object.keys(row.labels);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(new Set(labels.flat()));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
calcValueOptions(key) {
|
||||||
|
const valueOptions = [];
|
||||||
|
|
||||||
|
this.rows.map((row) => {
|
||||||
|
const isExistValue = valueOptions.find(value => value.label === row.labels[key]);
|
||||||
|
|
||||||
|
if (Object.keys(row.labels).includes(key) && key && row.labels[key] && !isExistValue) {
|
||||||
|
valueOptions.push({
|
||||||
|
value: row.labels[key],
|
||||||
|
label: row.labels[key]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return valueOptions;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAll() {
|
||||||
|
this.$set(this, 'searchLabels', []);
|
||||||
|
this.filterRows();
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(label) {
|
||||||
|
this.searchLabels.find((L, index) => {
|
||||||
|
if (L.key === label.key && L.value === label.value) {
|
||||||
|
this.searchLabels.splice(index, 1);
|
||||||
|
this.filterRows();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
filterRows() {
|
||||||
|
const rows = this.rows.filter((row) => {
|
||||||
|
const hasSearch = this.searchLabels.find(search => search.key);
|
||||||
|
|
||||||
|
if (!hasSearch) {
|
||||||
|
return this.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = row.labels;
|
||||||
|
const keys = Object.keys(labels);
|
||||||
|
|
||||||
|
return this.searchLabels.find((search) => {
|
||||||
|
if (search.key && keys.includes(search.key)) {
|
||||||
|
if (!search.value) { // If value is empty, all data containing the key is retained
|
||||||
|
return true;
|
||||||
|
} else if (search.value === labels[search.key]) {
|
||||||
|
return true;
|
||||||
|
} else if (search.value !== labels[search.key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$emit('changeRows', rows, this.searchLabels);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
rows: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.filterRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="filter">
|
||||||
|
<template v-for="(label, index) in searchLabels">
|
||||||
|
<span v-if="label.key" :key="`${label.key}${index}`" class="banner-item bg-warning">
|
||||||
|
{{ label.key }}{{ label.value ? "=" : '' }}{{ label.value }}<i class="icon icon-close" @click="remove(label)" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-popover
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom-end"
|
||||||
|
>
|
||||||
|
<slot name="header">
|
||||||
|
<button ref="actionDropDown" class="btn bg-primary mr-10">
|
||||||
|
<slot name="title">
|
||||||
|
{{ t('harvester.fields.filterLabels') }}
|
||||||
|
</slot>
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<template slot="popover">
|
||||||
|
<div class="filter-popup">
|
||||||
|
<div>
|
||||||
|
<ArrayList
|
||||||
|
v-model="searchLabels"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:initial-empty-row="true"
|
||||||
|
@input="filterRows"
|
||||||
|
>
|
||||||
|
<template v-slot:column-headers>
|
||||||
|
<div class="box">
|
||||||
|
<div class="key">
|
||||||
|
{{ t('generic.key') }}
|
||||||
|
<span class="required">*</span>
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ t('generic.value') }}
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<Select
|
||||||
|
ref="select"
|
||||||
|
key="label"
|
||||||
|
v-model="scope.row.value.key"
|
||||||
|
:append-to-body="false"
|
||||||
|
:searchable="true"
|
||||||
|
:options="optionLabels"
|
||||||
|
@input="filterRows"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
<Select
|
||||||
|
v-if="calcValueOptions(scope.row.value.key).length > 0"
|
||||||
|
ref="select"
|
||||||
|
key="value"
|
||||||
|
v-model="scope.row.value.value"
|
||||||
|
:append-to-body="false"
|
||||||
|
:searchable="true"
|
||||||
|
:options="calcValueOptions(scope.row.value.key)"
|
||||||
|
@input="filterRows"
|
||||||
|
/>
|
||||||
|
<LabeledInput v-else v-model="scope.row.value.value" @input="filterRows" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #add="{add}">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
data-testid="add-item"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
|
{{ t('generic.add') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
data-testid="remove-all-item"
|
||||||
|
@click="removeAll()"
|
||||||
|
>
|
||||||
|
{{ t('generic.clearAll') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.banner-item {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter-popup {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .box {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40% 40% 10%;
|
||||||
|
column-gap: 1.75%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { HCI } from '../types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { HCI } from '../types';
|
|
||||||
import UpgradeInfo from './UpgradeInfo';
|
import UpgradeInfo from './UpgradeInfo';
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgrade',
|
name: 'HarvesterUpgrade',
|
||||||
@ -34,7 +34,8 @@ export default {
|
|||||||
selectMode: true,
|
selectMode: true,
|
||||||
version: '',
|
version: '',
|
||||||
enableLogging: true,
|
enableLogging: true,
|
||||||
readyReleaseNote: false
|
readyReleaseNote: false,
|
||||||
|
isOpen: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ export default {
|
|||||||
versionOptions() {
|
versionOptions() {
|
||||||
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
|
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
|
||||||
|
|
||||||
return versions.map(V => V.metadata.name);
|
return versions.map((V) => V.metadata.name);
|
||||||
},
|
},
|
||||||
|
|
||||||
currentVersion() {
|
currentVersion() {
|
||||||
@ -68,7 +69,7 @@ export default {
|
|||||||
let upgradeMessage = [];
|
let upgradeMessage = [];
|
||||||
const list = neu || [];
|
const list = neu || [];
|
||||||
|
|
||||||
const currentResource = list.find( O => !!O.isLatestUpgrade);
|
const currentResource = list.find( (O) => !!O.isLatestUpgrade);
|
||||||
|
|
||||||
upgradeMessage = currentResource ? currentResource.upgradeMessage : [];
|
upgradeMessage = currentResource ? currentResource.upgradeMessage : [];
|
||||||
|
|
||||||
@ -111,12 +112,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$refs.deleteTip.hide();
|
this.isOpen = false;
|
||||||
this.errors = '';
|
this.errors = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
this.$refs.deleteTip.open();
|
this.isOpen = true;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -131,12 +132,21 @@ export default {
|
|||||||
:cluster="currentCluster.nameDisplay"
|
:cluster="currentCluster.nameDisplay"
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
<button v-if="versionOptions.length" type="button" class="btn bg-warning btn-sm" @click="open">
|
<button
|
||||||
|
v-if="versionOptions.length"
|
||||||
|
type="button"
|
||||||
|
class="btn bg-warning btn-sm"
|
||||||
|
@click="open"
|
||||||
|
>
|
||||||
<t k="harvester.upgradePage.upgrade" />
|
<t k="harvester.upgradePage.upgrade" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ModalWithCard ref="deleteTip" name="deleteTip" :width="850">
|
<ModalWithCard
|
||||||
|
v-if="isOpen"
|
||||||
|
name="deleteTip"
|
||||||
|
:width="850"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<t k="harvester.upgradePage.upgradeApp" />
|
<t k="harvester.upgradePage.upgradeApp" />
|
||||||
</template>
|
</template>
|
||||||
@ -158,17 +168,36 @@ export default {
|
|||||||
:clearable="true"
|
:clearable="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="canEnableLogging" class="mb-5">
|
<div
|
||||||
<Checkbox v-model="enableLogging" class="check" type="checkbox" :label="t('harvester.upgradePage.enableLogging')" />
|
v-if="canEnableLogging"
|
||||||
|
class="mb-5"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
v-model="enableLogging"
|
||||||
|
class="check"
|
||||||
|
type="checkbox"
|
||||||
|
:label="t('harvester.upgradePage.enableLogging')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="version">
|
<div v-if="version">
|
||||||
<p v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)" class="mb-10"></p>
|
<p
|
||||||
|
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
|
||||||
|
class="mb-10"
|
||||||
|
></p>
|
||||||
|
|
||||||
<Checkbox v-model="readyReleaseNote" class="check" type="checkbox" label-key="harvester.upgradePage.checkReady" />
|
<Checkbox
|
||||||
|
v-model="readyReleaseNote"
|
||||||
|
class="check"
|
||||||
|
type="checkbox"
|
||||||
|
label-key="harvester.upgradePage.checkReady"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Banner v-if="errors.length" color="warning">
|
<Banner
|
||||||
|
v-if="errors.length"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
{{ errors }}
|
{{ errors }}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
@ -176,10 +205,17 @@ export default {
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<button class="btn role-secondary mr-20" @click.prevent="cancel">
|
<button
|
||||||
|
class="btn role-secondary mr-20"
|
||||||
|
@click.prevent="cancel"
|
||||||
|
>
|
||||||
<t k="generic.close" />
|
<t k="generic.close" />
|
||||||
</button>
|
</button>
|
||||||
<button :disabled="!readyReleaseNote" class="btn role-tertiary bg-primary" @click.prevent="handleUpgrade">
|
<button
|
||||||
|
:disabled="!readyReleaseNote"
|
||||||
|
class="btn role-tertiary bg-primary"
|
||||||
|
@click.prevent="handleUpgrade"
|
||||||
|
>
|
||||||
<t k="harvester.upgradePage.upgrade" />
|
<t k="harvester.upgradePage.upgrade" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { NODE } from '@shell/config/types';
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import PercentageBar from '@shell/components/PercentageBar';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
import BadgeStateFormatter from '@shell/components/formatter/BadgeStateFormatter';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { PRODUCT_NAME as HARVESTER } from '../config/harvester';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import ProgressBarList from './HarvesterUpgradeProgressBarList';
|
import ProgressBarList from './HarvesterUpgradeProgressBarList';
|
||||||
|
import BadgeStateFormatter from '@shell/components/formatter/BadgeStateFormatter';
|
||||||
|
import { PRODUCT_NAME as HARVESTER } from '../config/harvester';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgradeHeader',
|
name: 'HarvesterUpgradeHeader',
|
||||||
|
|||||||
268
pkg/harvester/components/SerialConsole/index.vue
Normal file
268
pkg/harvester/components/SerialConsole/index.vue
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<script>
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
|
import Socket, {
|
||||||
|
EVENT_CONNECTED,
|
||||||
|
EVENT_CONNECTING,
|
||||||
|
EVENT_DISCONNECTED,
|
||||||
|
EVENT_MESSAGE,
|
||||||
|
EVENT_CONNECT_ERROR,
|
||||||
|
} from '@shell/utils/socket';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
socket: null,
|
||||||
|
terminal: null,
|
||||||
|
fitAddon: null,
|
||||||
|
searchAddon: null,
|
||||||
|
webglAddon: null,
|
||||||
|
isOpen: false,
|
||||||
|
isOpening: false,
|
||||||
|
backlog: [],
|
||||||
|
firstTime: true,
|
||||||
|
queue: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
xtermConfig() {
|
||||||
|
return {
|
||||||
|
allowProposedApi: true,
|
||||||
|
cursorBlink: true,
|
||||||
|
useStyle: true,
|
||||||
|
fontSize: 12,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
queue: {
|
||||||
|
handler: debounce(async function(neu) {
|
||||||
|
if (neu.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = await Promise.all(neu);
|
||||||
|
|
||||||
|
(msg || []).forEach((m) => {
|
||||||
|
this.terminal.write(m);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.queue = [];
|
||||||
|
}, 5)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.setupTerminal();
|
||||||
|
await this.connect();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async setupTerminal() {
|
||||||
|
const docStyle = getComputedStyle(document.querySelector('body'));
|
||||||
|
const xterm = await import(/* webpackChunkName: "xterm" */ 'xterm');
|
||||||
|
|
||||||
|
const addons = await allHash({
|
||||||
|
fit: import(/* webpackChunkName: "xterm" */ 'xterm-addon-fit'),
|
||||||
|
webgl: import(/* webpackChunkName: "xterm" */ 'xterm-addon-webgl'),
|
||||||
|
weblinks: import(/* webpackChunkName: "xterm" */ 'xterm-addon-web-links'),
|
||||||
|
search: import(/* webpackChunkName: "xterm" */ 'xterm-addon-search'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const terminal = new xterm.Terminal({
|
||||||
|
theme: {
|
||||||
|
background: docStyle.getPropertyValue('--terminal-bg').trim(),
|
||||||
|
cursor: docStyle.getPropertyValue('--terminal-cursor').trim(),
|
||||||
|
foreground: docStyle.getPropertyValue('--terminal-text').trim()
|
||||||
|
},
|
||||||
|
...this.xtermConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fitAddon = new addons.fit.FitAddon();
|
||||||
|
this.searchAddon = new addons.search.SearchAddon();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.webglAddon = new addons.webgl.WebGlAddon();
|
||||||
|
} catch (e) {
|
||||||
|
// Some browsers (Safari) don't support the webgl renderer, so don't use it.
|
||||||
|
this.webglAddon = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.loadAddon(this.fitAddon);
|
||||||
|
terminal.loadAddon(this.searchAddon);
|
||||||
|
terminal.loadAddon(new addons.weblinks.WebLinksAddon());
|
||||||
|
terminal.open(this.$refs.xterm);
|
||||||
|
|
||||||
|
if ( this.webglAddon ) {
|
||||||
|
terminal.loadAddon(this.webglAddon);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fit();
|
||||||
|
this.flush();
|
||||||
|
|
||||||
|
terminal.onData((input) => {
|
||||||
|
const msg = this.str2ab(input);
|
||||||
|
|
||||||
|
this.write(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.terminal = terminal;
|
||||||
|
},
|
||||||
|
|
||||||
|
str2ab(str) {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
|
||||||
|
return enc.encode(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
write(msg) {
|
||||||
|
if ( this.isOpen ) {
|
||||||
|
this.socket.send(msg);
|
||||||
|
} else {
|
||||||
|
this.backlog.push(msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.terminal.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
getSocketUrl() {
|
||||||
|
return `${ this.value?.getSerialConsolePath }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if ( this.socket ) {
|
||||||
|
await this.socket.disconnect();
|
||||||
|
this.socket = null;
|
||||||
|
this.terminal.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.getSocketUrl();
|
||||||
|
|
||||||
|
if ( !url ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket = new Socket(url);
|
||||||
|
|
||||||
|
this.socket.addEventListener(EVENT_CONNECTING, (e) => {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.isOpening = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.addEventListener(EVENT_CONNECT_ERROR, (e) => {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.isOpening = false;
|
||||||
|
console.error('Connect Error', e); // eslint-disable-line no-console
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.addEventListener(EVENT_CONNECTED, (e) => {
|
||||||
|
this.isOpen = true;
|
||||||
|
this.isOpening = false;
|
||||||
|
if (this.show) {
|
||||||
|
this.fit();
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.firstTime) {
|
||||||
|
this.socket.send(this.str2ab('\n'));
|
||||||
|
this.firstTime = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.addEventListener(EVENT_DISCONNECTED, (e) => {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.isOpening = false;
|
||||||
|
this.$emit('close');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.addEventListener(EVENT_MESSAGE, (e) => {
|
||||||
|
this.queue.push(e.detail.data.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.connect();
|
||||||
|
this.terminal.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
const backlog = this.backlog.slice();
|
||||||
|
|
||||||
|
this.backlog = [];
|
||||||
|
|
||||||
|
for ( const data of backlog ) {
|
||||||
|
this.socket.send(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fit(arg) {
|
||||||
|
if ( !this.fitAddon ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fitAddon.fit();
|
||||||
|
|
||||||
|
const { rows, cols } = this.fitAddon.proposeDimensions();
|
||||||
|
|
||||||
|
if ( !this.isOpen ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = JSON.stringify({
|
||||||
|
Width: cols,
|
||||||
|
Height: rows
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.send(this.str2ab(message));
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if ( this.socket ) {
|
||||||
|
this.socket.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.terminal ) {
|
||||||
|
this.terminal.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="harvester-shell-container">
|
||||||
|
<div ref="xterm" class="shell-body" />
|
||||||
|
<resize-observer @notify="fit" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../../../node_modules/xterm/css/xterm.css';
|
||||||
|
|
||||||
|
body, #__nuxt, #__layout, MAIN {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.harvester-shell-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.shell-body, .terminal.xterm {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
203
pkg/harvester/components/SettingList.vue
Normal file
203
pkg/harvester/components/SettingList.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
||||||
|
|
||||||
|
const CATEGORY = {
|
||||||
|
ui: [
|
||||||
|
'branding',
|
||||||
|
'ui-source',
|
||||||
|
'ui-plugin-index',
|
||||||
|
'ui-index',
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SettingLists',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
settings: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const categorySettings = this.settings.filter((s) => {
|
||||||
|
if (this.category !== 'advanced') {
|
||||||
|
return (CATEGORY[this.category] || []).find(item => item === s.id);
|
||||||
|
} else if (this.category === 'advanced') {
|
||||||
|
const allCategory = Object.keys(CATEGORY);
|
||||||
|
|
||||||
|
return !allCategory.some(category => (CATEGORY[category] || []).find(item => item === s.id));
|
||||||
|
}
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
HCI_SETTING,
|
||||||
|
categorySettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
showActionMenu(e, setting) {
|
||||||
|
const actionElement = e.srcElement;
|
||||||
|
|
||||||
|
this.$store.commit(`action-menu/show`, {
|
||||||
|
resources: setting.data,
|
||||||
|
elem: actionElement
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getSettingOption(id) {
|
||||||
|
return HCI_ALLOWED_SETTINGS.find(setting => setting.id === id);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleHide(s) {
|
||||||
|
this.categorySettings.find((setting) => {
|
||||||
|
if (setting.id === s.id) {
|
||||||
|
setting.hide = !setting.hide;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async testConnect(buttonDone, value) {
|
||||||
|
try {
|
||||||
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/backuptarget/healthz');
|
||||||
|
|
||||||
|
const health = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
const settingValue = JSON.parse(value);
|
||||||
|
|
||||||
|
if (health?._status === 200) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('harvester.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.backup.message.testConnect.successMessage', { endpoint: settingValue?.endpoint })
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
buttonDone(true);
|
||||||
|
} catch (err) {
|
||||||
|
if (err?._status === 400 || err?._status === 503) {
|
||||||
|
this.$store.dispatch('growl/error', {
|
||||||
|
title: this.t('harvester.notification.title.error'),
|
||||||
|
message: err?.errors[0]
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="setting in categorySettings" :key="setting.id" class="advanced-setting mb-20">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<h1>
|
||||||
|
{{ setting.id }}
|
||||||
|
<span v-if="setting.customized" class="modified">
|
||||||
|
Modified
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div v-if="setting.hasActions" :id="setting.id" class="action">
|
||||||
|
<button aria-haspopup="true" aria-expanded="false" type="button" class="btn btn-sm role-multi-action actions" @click="showActionMenu($event, setting)">
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div value>
|
||||||
|
<div v-if="!setting.hide" class="settings-value">
|
||||||
|
<pre v-if="setting.kind === 'json'">{{ setting.json }}</pre>
|
||||||
|
<pre v-else-if="setting.kind === 'multiline'">{{ setting.data.value || setting.data.default }}</pre>
|
||||||
|
<pre v-else-if="setting.kind === 'enum'">{{ t(setting.enum) }}</pre>
|
||||||
|
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
|
||||||
|
<pre v-else-if="setting.data.value || setting.data.default">{{ setting.data.value || setting.data.default }}</pre>
|
||||||
|
<pre v-else class="text-muted"><{{ t('advancedSettings.none') }}></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<button v-if="setting.hide" class="btn btn-sm role-primary" @click="toggleHide(setting)">
|
||||||
|
{{ t('advancedSettings.show') }} {{ setting.id }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="setting.canHide && !setting.hide" class="btn btn-sm role-primary" @click="toggleHide(setting)">
|
||||||
|
{{ t('advancedSettings.hide') }} {{ setting.id }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
v-if="setting.id === HCI_SETTING.BACKUP_TARGET"
|
||||||
|
class="backupButton ml-5"
|
||||||
|
mode="apply"
|
||||||
|
size="sm"
|
||||||
|
:delay="0"
|
||||||
|
:action-label="t('harvester.backup.message.testConnect.actionLabel')"
|
||||||
|
:waiting-label="t('harvester.backup.message.testConnect.waitingLabel')"
|
||||||
|
:success-label="t('harvester.backup.message.testConnect.successLabel')"
|
||||||
|
@click="(buttonCb) => testConnect(buttonCb, setting.data.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Banner v-if="setting.data.errMessage" color="error mt-5" class="settings-banner">
|
||||||
|
{{ setting.data.errMessage }}
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.settings-banner {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.advanced-setting {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-value pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modified {
|
||||||
|
margin-left: 10px;
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
pkg/harvester/components/VMConsoleBar.vue
Normal file
95
pkg/harvester/components/VMConsoleBar.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script>
|
||||||
|
import ButtonDropdown from '@shell/components/ButtonDropdown';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { OFF } from '../models/kubevirt.io.virtualmachine';
|
||||||
|
import { PRODUCT_NAME } from '../config/harvester';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMConsoleBar',
|
||||||
|
|
||||||
|
components: { ButtonDropdown },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resource: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
isOff() {
|
||||||
|
return this.resource.stateDisplay === OFF;
|
||||||
|
},
|
||||||
|
|
||||||
|
options() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: this.t('harvester.virtualMachine.console.novnc'),
|
||||||
|
value: 'vnc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.t('harvester.virtualMachine.console.serial'),
|
||||||
|
value: 'serial'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleDropdown(c) {
|
||||||
|
this.show(c.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
show(type) {
|
||||||
|
let uid = this.resource.metadata?.ownerReferences?.[0]?.uid;
|
||||||
|
|
||||||
|
if (uid === undefined) {
|
||||||
|
uid = this.resource.metadata.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = window.location.host;
|
||||||
|
const prefix = window.location.pathname.replace(this.$route.path, '');
|
||||||
|
const params = this.$route?.params;
|
||||||
|
|
||||||
|
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
url,
|
||||||
|
'_blank',
|
||||||
|
'toolbars=0,width=900,height=700,left=0,top=0,noreferrer'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmpty(o) {
|
||||||
|
return o !== undefined && Object.keys(o).length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="overview-web-console">
|
||||||
|
<ButtonDropdown
|
||||||
|
:disabled="isOff"
|
||||||
|
:no-drop="isOff"
|
||||||
|
button-label="Console"
|
||||||
|
:dropdown-options="options"
|
||||||
|
size="sm"
|
||||||
|
@click-action="handleDropdown"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.overview-web-console {
|
||||||
|
.btn {
|
||||||
|
line-height: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
149
pkg/harvester/components/novnc/NovncConsole.vue
Normal file
149
pkg/harvester/components/novnc/NovncConsole.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="connected && disconnected">
|
||||||
|
<main class="main-layout error">
|
||||||
|
<div class="text-center">
|
||||||
|
<BrandImage file-name="error-desert-landscape.svg" width="900" height="300" />
|
||||||
|
<h1>
|
||||||
|
{{ t('generic.notification.title.warning') }}
|
||||||
|
</h1>
|
||||||
|
<h2 class="text-secondary mt-20">
|
||||||
|
{{ t('vncConsole.error.message') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<div v-if="reconnecting">
|
||||||
|
<main class="main-layout">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-secondary mt-20">
|
||||||
|
{{ t('vncConsole.reconnecting.message') }}:{{ retryTimes }} of {{ maximumRetryTimes }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="view"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RFB from '@novnc/novnc/core/rfb';
|
||||||
|
import BrandImage from '@shell/components/BrandImage';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: { BrandImage },
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rfb: null,
|
||||||
|
connected: false,
|
||||||
|
disconnected: false,
|
||||||
|
reconnectDelay: 3000,
|
||||||
|
reconnecting: false,
|
||||||
|
maximumRetryTimes: 10,
|
||||||
|
retryTimes: 0,
|
||||||
|
setTimeout: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.connect();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.clearTimeout();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
connect() {
|
||||||
|
const rfb = new RFB(this.$refs.view, this.url);
|
||||||
|
|
||||||
|
rfb.addEventListener('connect', () => {
|
||||||
|
this.clearTimeout();
|
||||||
|
|
||||||
|
this.connected = true;
|
||||||
|
this.retryTimes = 0;
|
||||||
|
this.reconnecting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
rfb.addEventListener('disconnect', (e) => {
|
||||||
|
this.clearTimeout();
|
||||||
|
|
||||||
|
this.disconnected = true;
|
||||||
|
this.rfb = null;
|
||||||
|
this.reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rfb = rfb;
|
||||||
|
},
|
||||||
|
|
||||||
|
reconnect() {
|
||||||
|
if (this.retryTimes >= this.maximumRetryTimes) {
|
||||||
|
this.reconnecting = false;
|
||||||
|
this.connected = true;
|
||||||
|
this.disconnected = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.retryTimes += 1;
|
||||||
|
this.reconnecting = true;
|
||||||
|
this.connected = false;
|
||||||
|
this.disconnected = false;
|
||||||
|
|
||||||
|
this.setTimeout = setTimeout(() => {
|
||||||
|
this.connect();
|
||||||
|
}, this.reconnectDelay);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTimeout() {
|
||||||
|
if (this.setTimeout) {
|
||||||
|
clearTimeout(this.setTimeout);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.rfb.disconnect();
|
||||||
|
},
|
||||||
|
|
||||||
|
ctrlAltDelete() {
|
||||||
|
this.rfb.sendCtrlAltDel();
|
||||||
|
},
|
||||||
|
|
||||||
|
sendKey(keysym, code, down) {
|
||||||
|
this.rfb.sendKey(keysym, code, down);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.error {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desert-landscape {
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
245
pkg/harvester/components/novnc/NovncConsoleCustomKeys.vue
Normal file
245
pkg/harvester/components/novnc/NovncConsoleCustomKeys.vue
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<script>
|
||||||
|
import { STEVE } from '@shell/config/types';
|
||||||
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
|
||||||
|
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalWithCard, Banner, AsyncButton
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keysRecord: [],
|
||||||
|
addedShortcutKeys: [],
|
||||||
|
preferredShortcutKeys: [],
|
||||||
|
isRecording: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
savedShortcutKeys() {
|
||||||
|
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE);
|
||||||
|
const preferedShortcutKeys = preference?.[0]?.data?.[PREFERED_SHORTCUT_KEYS];
|
||||||
|
let out = [];
|
||||||
|
|
||||||
|
if (!preferedShortcutKeys) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
out = JSON.parse(preferedShortcutKeys);
|
||||||
|
} catch (err) {
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: this.t('harvester.virtualMachine.detail.console.customShortcutKeys') }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
displayedKeys() {
|
||||||
|
const out = this.addedShortcutKeys.concat(this.preferredShortcutKeys).map((item) => {
|
||||||
|
const out = item.map(K => ` <code>${ K.key.charAt(0).toUpperCase() + K.key.slice(1) }</code>`);
|
||||||
|
|
||||||
|
return out.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
recordButton() {
|
||||||
|
if (this.isRecording) {
|
||||||
|
return 'harvester.virtualMachine.detail.console.record.stop';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'harvester.virtualMachine.detail.console.record.start';
|
||||||
|
},
|
||||||
|
|
||||||
|
keysRecordFormat() {
|
||||||
|
if (!this.isRecording && this.keysRecord.length === 0) {
|
||||||
|
return this.t('harvester.virtualMachine.detail.console.record.tips');
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = this.keysRecord.map(item => ` <code>${ item.key.charAt(0).toUpperCase() + item.key.slice(1) }</code>`);
|
||||||
|
|
||||||
|
return `Keys: ${ out.join(',') }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
canAdd() {
|
||||||
|
const hasRecord = this.keysRecord.length > 0;
|
||||||
|
let validationList = [].concat(this.preferredShortcutKeys, this.addedShortcutKeys);
|
||||||
|
|
||||||
|
if (!hasRecord) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationList.push(this.keysRecord);
|
||||||
|
|
||||||
|
validationList = validationList.map((item) => {
|
||||||
|
const out = item.map(K => K.key);
|
||||||
|
|
||||||
|
return out.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
return validationList.length === new Set(validationList).size;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
savedShortcutKeys: {
|
||||||
|
handler() {
|
||||||
|
this.preferredShortcutKeys = [].concat(this.savedShortcutKeys) || [];
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
show() {
|
||||||
|
this.$refs.recordShortcutKeys.open();
|
||||||
|
},
|
||||||
|
|
||||||
|
closeRecordingModal() {
|
||||||
|
window.removeEventListener('keydown', this.handleShortcut);
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleRecording() {
|
||||||
|
this.isRecording = !this.isRecording;
|
||||||
|
|
||||||
|
if (this.isRecording) {
|
||||||
|
this.keysRecord = [];
|
||||||
|
window.addEventListener('keydown', this.handleShortcut);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('keydown', this.handleShortcut);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleShortcut(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const {
|
||||||
|
key, keyCode, code, location, charCode
|
||||||
|
} = event;
|
||||||
|
|
||||||
|
this.keysRecord.push({
|
||||||
|
key, keyCode, code, location, charCode
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addShortcutKey() {
|
||||||
|
this.addedShortcutKeys.push([].concat(this.keysRecord));
|
||||||
|
},
|
||||||
|
|
||||||
|
removeKey(keys) {
|
||||||
|
const key = keys.replace(/(\s*)<code>|<\/code>/g, '').replace(/\s*,\s*/g, ',');
|
||||||
|
|
||||||
|
this.addedShortcutKeys = this.addedShortcutKeys.filter((item) => {
|
||||||
|
const formatkey = item.map(K => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join(',');
|
||||||
|
|
||||||
|
return formatkey !== key;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.preferredShortcutKeys = this.preferredShortcutKeys.filter((item) => {
|
||||||
|
const formatkey = item.map(K => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join(',');
|
||||||
|
|
||||||
|
return formatkey !== key;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveKeys(buttonCb) {
|
||||||
|
const out = [].concat(this.preferredShortcutKeys, this.addedShortcutKeys);
|
||||||
|
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE)?.[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.$set(preference.data, PREFERED_SHORTCUT_KEYS, JSON.stringify(out));
|
||||||
|
await preference.save();
|
||||||
|
this.closeRecordingModal();
|
||||||
|
buttonCb(true);
|
||||||
|
} catch (err) {
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalWithCard ref="recordShortcutKeys" name="recordShortcutKeys" :width="550">
|
||||||
|
<template #title>
|
||||||
|
<t k="harvester.virtualMachine.detail.console.customShortcutKeys" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner color="info">
|
||||||
|
<span v-clean-html="keysRecordFormat"></span>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<button class="btn bg-primary" @click="toggleRecording">
|
||||||
|
<t :k="recordButton" />
|
||||||
|
<i class="icon icon-fw" :class="isRecording ? 'icon-dot-open' : 'icon-dot'" />
|
||||||
|
</button>
|
||||||
|
<button :disabled="!canAdd" class="btn bg-primary" @click="addShortcutKey">
|
||||||
|
<t k="generic.add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="displayed-keys mt-20">
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.virtualMachine.detail.console.record.preferredKeys')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="displayed-banners">
|
||||||
|
<Banner v-for="(keys,index) in displayedKeys" :key="index" color="info" :closable="true" @close="removeKey(keys)">
|
||||||
|
<span v-clean-html="keys"></span>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn role-secondary mr-20" @click.prevent="closeRecordingModal">
|
||||||
|
<t k="generic.close" />
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
mode="done"
|
||||||
|
@click="saveKeys"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ModalWithCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.displayed-keys {
|
||||||
|
.banner {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayed-banners {
|
||||||
|
max-height: 155px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
89
pkg/harvester/components/novnc/NovncConsoleItem.vue
Normal file
89
pkg/harvester/components/novnc/NovncConsoleItem.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NovncConsoleItem',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
path: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
pos: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
keysDown(key, pos) {
|
||||||
|
this.addKeys({ key, pos });
|
||||||
|
this.$emit('send-keys');
|
||||||
|
},
|
||||||
|
|
||||||
|
addKeys({ key, pos }) {
|
||||||
|
this.$emit('update', { key, pos });
|
||||||
|
},
|
||||||
|
|
||||||
|
sendKeys() {
|
||||||
|
this.$emit('send-keys');
|
||||||
|
},
|
||||||
|
|
||||||
|
getOpenStatus(key, pos) {
|
||||||
|
return this.path[pos] === key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul class="list-unstyled dropdown combination-keys__container">
|
||||||
|
<li v-for="(item, key) in items" :key="key">
|
||||||
|
<v-popover
|
||||||
|
v-if="!!item.keys"
|
||||||
|
placement="right-start"
|
||||||
|
trigger="click"
|
||||||
|
:container="false"
|
||||||
|
>
|
||||||
|
<span :class="{ open: getOpenStatus(key, pos) }" class="p-10 hand" @click="addKeys({ key, pos })">{{ item.label }}</span>
|
||||||
|
|
||||||
|
<template v-slot:popover>
|
||||||
|
<novnc-console-item :items="item.keys" :path="path" :pos="pos+1" @update="addKeys" @send-keys="sendKeys" />
|
||||||
|
</template>
|
||||||
|
</v-popover>
|
||||||
|
|
||||||
|
<span v-else class="p-10 hand" @click="keysDown(key, pos)">{{ item.label }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.combination-keys__container {
|
||||||
|
max-width: 60px;
|
||||||
|
|
||||||
|
DIV, SPAN {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPAN {
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover, &.open {
|
||||||
|
color: var(--primary-hover-text);
|
||||||
|
background: var(--primary-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
348
pkg/harvester/components/novnc/NovncConsoleWrapper.vue
Normal file
348
pkg/harvester/components/novnc/NovncConsoleWrapper.vue
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
<script>
|
||||||
|
import { STEVE } from '@shell/config/types';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import KeyTable from '@novnc/novnc/core/input/keysym';
|
||||||
|
import * as KeyboardUtil from '@novnc/novnc/core/input/util';
|
||||||
|
import NovncConsole from './NovncConsole';
|
||||||
|
import NovncConsoleItem from './NovncConsoleItem';
|
||||||
|
import NovncConsoleCustomKeys from './NovncConsoleCustomKeys';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
|
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
|
||||||
|
|
||||||
|
const SHORT_KEYS = {
|
||||||
|
ControlLeft: {
|
||||||
|
label: 'Ctrl',
|
||||||
|
value: KeyTable.XK_Control_L,
|
||||||
|
},
|
||||||
|
AltLeft: {
|
||||||
|
label: 'Alt',
|
||||||
|
value: KeyTable.XK_Alt_L,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const FUNCTION_KEYS = {
|
||||||
|
Delete: {
|
||||||
|
label: 'Del',
|
||||||
|
value: KeyTable.XK_Delete,
|
||||||
|
},
|
||||||
|
PrintScreen: {
|
||||||
|
label: 'Print Screen',
|
||||||
|
value: KeyTable.XK_Print,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const NORMAL_KEYS = {
|
||||||
|
KeyN: {
|
||||||
|
label: 'N',
|
||||||
|
value: KeyTable.XK_n,
|
||||||
|
},
|
||||||
|
KeyT: {
|
||||||
|
label: 'T',
|
||||||
|
value: KeyTable.XK_t,
|
||||||
|
},
|
||||||
|
KeyW: {
|
||||||
|
label: 'W',
|
||||||
|
value: KeyTable.XK_w,
|
||||||
|
},
|
||||||
|
KeyY: {
|
||||||
|
label: 'Y',
|
||||||
|
value: KeyTable.XK_y,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const F_KEYS = {
|
||||||
|
F1: {
|
||||||
|
label: 'F1',
|
||||||
|
value: KeyTable.XK_F1,
|
||||||
|
},
|
||||||
|
F2: {
|
||||||
|
label: 'F2',
|
||||||
|
value: KeyTable.XK_F2,
|
||||||
|
},
|
||||||
|
F3: {
|
||||||
|
label: 'F3',
|
||||||
|
value: KeyTable.XK_F3,
|
||||||
|
},
|
||||||
|
F4: {
|
||||||
|
label: 'F4',
|
||||||
|
value: KeyTable.XK_F4,
|
||||||
|
},
|
||||||
|
F5: {
|
||||||
|
label: 'F5',
|
||||||
|
value: KeyTable.XK_F5,
|
||||||
|
},
|
||||||
|
F6: {
|
||||||
|
label: 'F6',
|
||||||
|
value: KeyTable.XK_F6,
|
||||||
|
},
|
||||||
|
F7: {
|
||||||
|
label: 'F7',
|
||||||
|
value: KeyTable.XK_F7,
|
||||||
|
},
|
||||||
|
F8: {
|
||||||
|
label: 'F8',
|
||||||
|
value: KeyTable.XK_F8,
|
||||||
|
},
|
||||||
|
F9: {
|
||||||
|
label: 'F9',
|
||||||
|
value: KeyTable.XK_F9,
|
||||||
|
},
|
||||||
|
F10: {
|
||||||
|
label: 'F10',
|
||||||
|
value: KeyTable.XK_F10,
|
||||||
|
},
|
||||||
|
F11: {
|
||||||
|
label: 'F11',
|
||||||
|
value: KeyTable.XK_F11,
|
||||||
|
},
|
||||||
|
F12: {
|
||||||
|
label: 'F12',
|
||||||
|
value: KeyTable.XK_F12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
NovncConsole, NovncConsoleItem, NovncConsoleCustomKeys
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const _hash = { vmResource: this.$store.dispatch('harvester/find', { type: HCI.VM, id: this.value.id }) };
|
||||||
|
|
||||||
|
const hash = await allHash(_hash);
|
||||||
|
|
||||||
|
this.vmResource = hash.vmResource;
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keysRecord: [],
|
||||||
|
vmResource: {},
|
||||||
|
renderKeysModal: false,
|
||||||
|
currentUser: null,
|
||||||
|
hideCustomKeysBar: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
savedShortcutKeys() {
|
||||||
|
const preference = this.$store.getters['management/all'](STEVE.PREFERENCE);
|
||||||
|
const preferedShortcutKeys = preference?.[0]?.data?.[PREFERED_SHORTCUT_KEYS];
|
||||||
|
let out = [];
|
||||||
|
|
||||||
|
if (!preference?.[0]?.data) {
|
||||||
|
this.hideCustomKeysBar = true;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preferedShortcutKeys) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
out = JSON.parse(preferedShortcutKeys);
|
||||||
|
} catch (err) {
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
isDown() {
|
||||||
|
return this.isEmpty(this.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
url() {
|
||||||
|
const ip = `${ window.location.hostname }:${ window.location.port }`;
|
||||||
|
|
||||||
|
return `wss://${ ip }${ this.value?.getVMIApiPath }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
allKeys() {
|
||||||
|
return {
|
||||||
|
...SHORT_KEYS,
|
||||||
|
...FUNCTION_KEYS,
|
||||||
|
...NORMAL_KEYS,
|
||||||
|
...F_KEYS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
keymap() {
|
||||||
|
const out = {
|
||||||
|
...SHORT_KEYS,
|
||||||
|
PrintScreen: FUNCTION_KEYS.PrintScreen,
|
||||||
|
...F_KEYS,
|
||||||
|
};
|
||||||
|
|
||||||
|
out.AltLeft.keys = { PrintScreen: FUNCTION_KEYS.PrintScreen, ...F_KEYS };
|
||||||
|
out.ControlLeft.keys = {
|
||||||
|
AltLeft: {
|
||||||
|
...Object.assign(SHORT_KEYS.AltLeft, {}),
|
||||||
|
keys: { Delete: FUNCTION_KEYS.Delete }
|
||||||
|
},
|
||||||
|
...NORMAL_KEYS,
|
||||||
|
};
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasSoftRebootAction() {
|
||||||
|
return !!this.vmResource?.actions?.softreboot;
|
||||||
|
},
|
||||||
|
|
||||||
|
preferredShortcutKeys() {
|
||||||
|
return (this.savedShortcutKeys || []).map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.map(K => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join('+'),
|
||||||
|
value: item
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
isEmpty(o) {
|
||||||
|
return o !== undefined && Object.keys(o).length === 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$refs.novncConsole.disconnect();
|
||||||
|
},
|
||||||
|
|
||||||
|
update({ key, pos }) {
|
||||||
|
this.keysRecord.splice(pos, this.keysRecord.length - pos, key);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Send function key, e.g. ALT + F
|
||||||
|
sendKeys() {
|
||||||
|
this.keysRecord.forEach((key) => {
|
||||||
|
this.$refs.novncConsole.sendKey(this.allKeys[key].value, key, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.keysRecord.reverse().forEach((key) => {
|
||||||
|
this.$refs.novncConsole.sendKey(this.allKeys[key].value, key, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$refs.popover.isOpen = false;
|
||||||
|
this.keysRecord = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
sendCustomKeys(keys) {
|
||||||
|
const keyList = [].concat(keys);
|
||||||
|
|
||||||
|
keyList.forEach((K) => {
|
||||||
|
this.$refs.novncConsole.sendKey(KeyboardUtil.getKeysym(K), KeyboardUtil.getKeycode(K), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
keyList.reverse().forEach((K) => {
|
||||||
|
this.$refs.novncConsole.sendKey(KeyboardUtil.getKeysym(K), KeyboardUtil.getKeycode(K), false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
softReboot() {
|
||||||
|
this.vmResource.softrebootVM();
|
||||||
|
},
|
||||||
|
|
||||||
|
showKeysModal() {
|
||||||
|
this.renderKeysModal = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.keysModal.show();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hideKeysModal() {
|
||||||
|
this.renderKeysModal = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<div class="vm-console">
|
||||||
|
<div class="combination-keys">
|
||||||
|
<v-popover
|
||||||
|
ref="popover"
|
||||||
|
placement="top"
|
||||||
|
trigger="click"
|
||||||
|
:container="false"
|
||||||
|
@auto-hide="keysRecord = []"
|
||||||
|
>
|
||||||
|
<button class="btn btn-sm bg-primary">
|
||||||
|
{{ t("harvester.virtualMachine.detail.console.shortcutKeys") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template v-slot:popover>
|
||||||
|
<novnc-console-item :items="keymap" :path="keysRecord" :pos="0" @update="update" @send-keys="sendKeys" />
|
||||||
|
</template>
|
||||||
|
</v-popover>
|
||||||
|
|
||||||
|
<button v-if="hasSoftRebootAction" class="btn btn-sm bg-primary" @click="softReboot">
|
||||||
|
{{ t("harvester.action.softreboot") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<v-popover
|
||||||
|
v-if="!hideCustomKeysBar"
|
||||||
|
ref="customKeyPopover"
|
||||||
|
placement="top"
|
||||||
|
trigger="click"
|
||||||
|
:container="false"
|
||||||
|
>
|
||||||
|
<button class="btn btn-sm bg-primary">
|
||||||
|
{{ t("harvester.virtualMachine.detail.console.customShortcutKeys") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template v-slot:popover>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm bg-primary" @click="showKeysModal">
|
||||||
|
{{ t("harvester.virtualMachine.detail.console.management") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div v-for="(keys, index) in preferredShortcutKeys" :key="index" class="mb-5">
|
||||||
|
<button class="btn btn-sm bg-primary" @click="sendCustomKeys(keys.value)">
|
||||||
|
{{ keys.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-popover>
|
||||||
|
|
||||||
|
<NovncConsoleCustomKeys v-if="renderKeysModal" ref="keysModal" :current-user="currentUser" @close="hideKeysModal" />
|
||||||
|
</div>
|
||||||
|
<NovncConsole v-if="url && !isDown" ref="novncConsole" :url="url" />
|
||||||
|
<p v-if="isDown">
|
||||||
|
{{ t("harvester.virtualMachine.detail.console.down") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vm-console {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 30px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combination-keys {
|
||||||
|
background: rgb(40, 40, 40);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
pkg/harvester/components/settings/additional-ca.vue
Normal file
36
pkg/harvester/components/settings/additional-ca.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
|
||||||
|
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterAdditionalCA',
|
||||||
|
|
||||||
|
components: { FileSelector, TextAreaAutoGrow },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
methods: { onKeySelected: createOnSelected('value.value') },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<FileSelector
|
||||||
|
class="btn btn-sm bg-primary mb-10"
|
||||||
|
:label="t('generic.readFromFile')"
|
||||||
|
@selected="onKeySelected"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextAreaAutoGrow
|
||||||
|
v-model="value.value"
|
||||||
|
:min-height="254"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
85
pkg/harvester/components/settings/auto-rotate-rke2-certs.vue
Normal file
85
pkg/harvester/components/settings/auto-rotate-rke2-certs.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterAutoRotateRKE2Certs',
|
||||||
|
|
||||||
|
components: { RadioGroup, UnitInput },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { parseDefaultValue };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
const parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="parseDefaultValue.enable"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-if="parseDefaultValue.enable"
|
||||||
|
v-model="parseDefaultValue.expiringInHours"
|
||||||
|
v-int-number
|
||||||
|
class="mb-20"
|
||||||
|
:min="1"
|
||||||
|
:max="8759"
|
||||||
|
:required="true"
|
||||||
|
:suffix="parseDefaultValue.expiringInHours === 1 ? 'Hour' : 'Hours'"
|
||||||
|
:label="t('harvester.setting.autoRotateRKE2Certs.expiringInHours')"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
199
pkg/harvester/components/settings/backup-target.vue
Normal file
199
pkg/harvester/components/settings/backup-target.vue
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<script>
|
||||||
|
import Tip from '@shell/components/Tip';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
import Password from '@shell/components/form/Password';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditBackupTarget',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput, LabeledSelect, Tip, Password, MessageLink
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = { type: '', endpoint: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseDefaultValue.type) {
|
||||||
|
parseDefaultValue.type = 's3';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
typeOption() {
|
||||||
|
return [{
|
||||||
|
value: 'nfs',
|
||||||
|
label: 'NFS'
|
||||||
|
}, {
|
||||||
|
value: 's3',
|
||||||
|
label: 'S3'
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
virtualHostedStyleType() {
|
||||||
|
return [{
|
||||||
|
value: true,
|
||||||
|
label: 'True'
|
||||||
|
}, {
|
||||||
|
value: false,
|
||||||
|
label: 'False'
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
isS3() {
|
||||||
|
return this.parseDefaultValue.type === 's3';
|
||||||
|
},
|
||||||
|
|
||||||
|
endpointPlaceholder() {
|
||||||
|
return this.isS3 ? '' : 'nfs://server:/path/';
|
||||||
|
},
|
||||||
|
|
||||||
|
toCA() {
|
||||||
|
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
let parseDefaultValue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
} catch (err) {
|
||||||
|
parseDefaultValue = { type: '', endpoint: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
if (!this.isS3) {
|
||||||
|
delete this.parseDefaultValue.accessKeyId;
|
||||||
|
delete this.parseDefaultValue.secretAccessKey;
|
||||||
|
delete this.parseDefaultValue.bucketName;
|
||||||
|
delete this.parseDefaultValue.bucketRegion;
|
||||||
|
delete this.parseDefaultValue.virtualHostedStyle;
|
||||||
|
delete this.parseDefaultValue.cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
if (!this.parseDefaultValue.type) {
|
||||||
|
this.$delete(this.value, 'value');
|
||||||
|
} else {
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
const parseDefaultValue = { type: '', endpoint: '' };
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row" @input="update">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect v-model="parseDefaultValue.type" class="mb-20" :label="t('harvester.fields.type')" :options="typeOption" @input="update" />
|
||||||
|
|
||||||
|
<LabeledInput v-model="parseDefaultValue.endpoint" class="mb-5" :placeholder="endpointPlaceholder" :mode="mode" label="Endpoint" />
|
||||||
|
<Tip class="mb-20" icon="icon icon-info" :text="t('harvester.backup.backupTargetTip')" />
|
||||||
|
|
||||||
|
<template v-if="isS3">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.bucketName"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
label="Bucket Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.bucketRegion"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
label="Bucket Region"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.accessKeyId"
|
||||||
|
:placeholder="t('harvester.setting.placeholder.accessKeyId')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
label="Access Key ID"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Password
|
||||||
|
v-model="parseDefaultValue.secretAccessKey"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.setting.placeholder.secretAccessKey')"
|
||||||
|
label="Secret Access Key"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect v-model="parseDefaultValue.virtualHostedStyle" class="mb-20" label="Virtual Hosted-Style" :options="virtualHostedStyleType" @input="update" />
|
||||||
|
|
||||||
|
<div class="mb-20">
|
||||||
|
<Tip icon="icon icon-info">
|
||||||
|
<MessageLink
|
||||||
|
:to="toCA"
|
||||||
|
target="_blank"
|
||||||
|
prefix-label="harvester.setting.message.ca.prefix"
|
||||||
|
middle-label="harvester.setting.message.ca.middle"
|
||||||
|
suffix-label="harvester.setting.message.ca.suffix"
|
||||||
|
/>
|
||||||
|
</Tip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
p {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.icon-h-question {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.tip {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goCA {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
377
pkg/harvester/components/settings/containerd-registry.vue
Normal file
377
pkg/harvester/components/settings/containerd-registry.vue
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterContainerdRegistry',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
InfoBox,
|
||||||
|
KeyValue,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const originMirror = {
|
||||||
|
Endpoints: [],
|
||||||
|
Rewrites: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const originConfig = {
|
||||||
|
Auth: {
|
||||||
|
Username: '',
|
||||||
|
Password: '',
|
||||||
|
Auth: '',
|
||||||
|
IdentityToken: ''
|
||||||
|
},
|
||||||
|
TLS: { InsecureSkipVerify: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
let originValue = {};
|
||||||
|
const baseValue = {
|
||||||
|
Mirrors: { '': clone(originMirror) },
|
||||||
|
Configs: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
originValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
originValue = baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(originValue).length) {
|
||||||
|
originValue = baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _mirrors = originValue.Mirrors || {};
|
||||||
|
const _configs = originValue.Configs || {};
|
||||||
|
const mirrorsKeys = Object.keys(_mirrors);
|
||||||
|
const configsKeys = Object.keys(_configs);
|
||||||
|
const mirrors = mirrorsKeys.map((key) => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: originValue.Mirrors[key],
|
||||||
|
idx: randomStr(5).toLowerCase()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const configs = configsKeys.map((key) => {
|
||||||
|
if (!originValue.Configs[key]?.Auth) {
|
||||||
|
originValue.Configs[key].Auth = originConfig.Auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: originValue.Configs[key],
|
||||||
|
idx: randomStr(5).toLowerCase()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
mirrors,
|
||||||
|
configs,
|
||||||
|
originMirror,
|
||||||
|
originConfig,
|
||||||
|
mirrorsKeys,
|
||||||
|
configsKeys,
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
insecureSkipVerifyOption() {
|
||||||
|
return [{
|
||||||
|
label: 'True',
|
||||||
|
value: true
|
||||||
|
}, {
|
||||||
|
label: 'False',
|
||||||
|
value: false
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
willSave() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (this.value.value) {
|
||||||
|
try {
|
||||||
|
JSON.parse(this.value.value);
|
||||||
|
|
||||||
|
this.mirrors.forEach((mirror) => {
|
||||||
|
if (!mirror.key) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.containerdRegistry.mirrors.registryName') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mirror.value.Endpoints.length === 0) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.containerdRegistry.mirrors.endpoints') }, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.configs.forEach((config) => {
|
||||||
|
if (!config.key) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.containerdRegistry.configs.registryEDQNorIP') }, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const _mirrors = {};
|
||||||
|
const _configs = {};
|
||||||
|
|
||||||
|
this.mirrors.forEach((mirror) => {
|
||||||
|
_mirrors[mirror.key] = mirror.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.configs.forEach((config) => {
|
||||||
|
_configs[config.key] = config.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
Mirrors: _mirrors,
|
||||||
|
Configs: _configs
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Object.keys(_mirrors).length) {
|
||||||
|
delete out.Mirrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(_configs).length) {
|
||||||
|
delete out.Configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Object.keys(out).length ? JSON.stringify(out) : '';
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
addMirror() {
|
||||||
|
this.mirrors.push({
|
||||||
|
key: '', value: clone(this.originMirror), idx: randomStr(5).toLowerCase()
|
||||||
|
});
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
addConfig() {
|
||||||
|
this.configs.push({
|
||||||
|
key: '', value: clone(this.originConfig), idx: randomStr(5).toLowerCase()
|
||||||
|
});
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(type, idx) {
|
||||||
|
this[type].splice(idx, 1);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(value) {
|
||||||
|
if (!value.value) { // useDefaultVale
|
||||||
|
this.$set(this, 'mirrors', []);
|
||||||
|
this.$set(this, 'configs', []);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>{{ t('harvester.setting.containerdRegistry.mirrors.mirrors') }}</h3>
|
||||||
|
<div>
|
||||||
|
<InfoBox v-for="mirror, idx in mirrors" :key="mirror.idx" class="box">
|
||||||
|
<button type="button" class="role-link btn btn-sm remove" @click="remove('mirrors', idx)">
|
||||||
|
<i class="icon icon-2x icon-x" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="mirror.key"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.containerdRegistry.mirrors.registryName"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-20">
|
||||||
|
<LabeledSelect
|
||||||
|
:key="mirror.idx"
|
||||||
|
v-model="mirror.value.Endpoints"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.containerdRegistry.mirrors.endpoints"
|
||||||
|
:multiple="true"
|
||||||
|
:taggable="true"
|
||||||
|
:searchable="true"
|
||||||
|
:options="[]"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<KeyValue
|
||||||
|
v-model="mirror.value.Rewrites"
|
||||||
|
:add-label="t('harvester.setting.containerdRegistry.mirrors.rewrite.addRewrite')"
|
||||||
|
:mode="mode"
|
||||||
|
:title="t('harvester.setting.containerdRegistry.mirrors.rewrite.rewrite')"
|
||||||
|
:read-allowed="false"
|
||||||
|
:value-can-be-empty="true"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</infobox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-sm role-primary" @click.self="addMirror">
|
||||||
|
{{ t('harvester.setting.containerdRegistry.mirrors.addMirror') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<hr class="divider mt-20 mb-20" />
|
||||||
|
|
||||||
|
<h3>{{ t('harvester.setting.containerdRegistry.configs.configs') }}</h3>
|
||||||
|
<div>
|
||||||
|
<InfoBox v-for="config, idx in configs" :key="config.idx" class="box">
|
||||||
|
<button type="button" class="role-link btn btn-sm remove" @click="remove('configs', idx)">
|
||||||
|
<i class="icon icon-2x icon-x" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="config.key"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.setting.containerdRegistry.configs.registryPlaceholder')"
|
||||||
|
label-key="harvester.setting.containerdRegistry.configs.registryEDQNorIP"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="config.value.Auth.Username"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.setting.containerdRegistry.configs.username"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="config.value.Auth.Password"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.setting.containerdRegistry.configs.password"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="config.value.Auth.Auth"
|
||||||
|
:mode="mode"
|
||||||
|
type="multiline"
|
||||||
|
:min-height="150"
|
||||||
|
label-key="harvester.setting.containerdRegistry.configs.auth"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="config.value.Auth.IdentityToken"
|
||||||
|
:mode="mode"
|
||||||
|
type="multiline"
|
||||||
|
:min-height="150"
|
||||||
|
label-key="harvester.setting.containerdRegistry.configs.identityToken"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="config.value.TLS.InsecureSkipVerify"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.setting.containerdRegistry.configs.insecureSkipVerify"
|
||||||
|
:options="insecureSkipVerifyOption"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</infobox>
|
||||||
|
|
||||||
|
<button class="btn btn-sm role-primary" @click="addConfig">
|
||||||
|
{{ t('harvester.setting.containerdRegistry.configs.addConfig') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.box {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
249
pkg/harvester/components/settings/csi-driver-config.vue
Normal file
249
pkg/harvester/components/settings/csi-driver-config.vue
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
<script>
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import { CSI_DRIVER, VOLUME_SNAPSHOT_CLASS } from '../../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
|
||||||
|
const LONGHORN_DRIVER = 'driver.longhorn.io';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterCsiDriver',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
InfoBox,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||||
|
volumeSnapshotClass: this.$store.dispatch(`${ inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS })
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const initValue = this.value.value || this.value.default;
|
||||||
|
const configArr = this.parseValue(initValue);
|
||||||
|
|
||||||
|
return { configArr };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
provisioners() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||||
|
|
||||||
|
return csiDrivers.filter((provisioner) => {
|
||||||
|
return !this.configArr.map(config => config.key).includes(provisioner.name);
|
||||||
|
}).map((provisioner) => {
|
||||||
|
return provisioner.name;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
disableAdd() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||||
|
|
||||||
|
return this.configArr.length >= csiDrivers.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getVolumeSnapshotOptions(provisioner) {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const volumeSnapshotClass = this.$store.getters[`${ inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||||
|
|
||||||
|
return volumeSnapshotClass.filter((snapshot) => {
|
||||||
|
return snapshot.driver === provisioner;
|
||||||
|
}).map((snapshot) => {
|
||||||
|
return {
|
||||||
|
label: snapshot.name,
|
||||||
|
value: snapshot.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
parseValue(value) {
|
||||||
|
const out = [];
|
||||||
|
let csiConfigJson = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
csiConfigJson = JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
new Error('json error');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(csiConfigJson)) {
|
||||||
|
out.push({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const out = {};
|
||||||
|
|
||||||
|
this.configArr.map((config) => {
|
||||||
|
out[config.key] = config.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = this.configArr.length ? JSON.stringify(out) : '';
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
this.update();
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.configArr.forEach((config) => {
|
||||||
|
if (!config.key) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.provisioner') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.value.volumeSnapshotClassName) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.volumeSnapshotClassName') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.value.backupVolumeSnapshotClassName) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName') }, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(idx) {
|
||||||
|
this.configArr.splice(idx, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
disableEdit(driver) {
|
||||||
|
return driver === LONGHORN_DRIVER;
|
||||||
|
},
|
||||||
|
|
||||||
|
add() {
|
||||||
|
this.configArr.push({
|
||||||
|
key: '',
|
||||||
|
value: { volumeSnapshotClassName: '', backupVolumeSnapshotClassName: '' }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
const configArr = this.parseValue(this.value.default);
|
||||||
|
|
||||||
|
this.$set(this, 'configArr', configArr);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<InfoBox v-for="(driver, idx) in configArr" :key="driver.key" class="box">
|
||||||
|
<button :disabled="disableEdit(driver.key)" type="button" class="role-link btn btn-sm remove" @click="remove(idx)">
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="driver.key"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
label-key="harvester.setting.csiDriverConfig.provisioner"
|
||||||
|
:searchable="true"
|
||||||
|
:options="provisioners"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="driver.value.volumeSnapshotClassName"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
:options="getVolumeSnapshotOptions(driver.key)"
|
||||||
|
:label="t('harvester.setting.csiDriverConfig.volumeSnapshotClassName')"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="driver.value.backupVolumeSnapshotClassName"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
:options="getVolumeSnapshotOptions(driver.key)"
|
||||||
|
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
|
||||||
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</infobox>
|
||||||
|
|
||||||
|
<button class="btn btn-sm role-primary" :disabled="disableAdd" @click.self="add">
|
||||||
|
{{ t('generic.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.box {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DefaultVMTerminationGracePeriodSeconds',
|
||||||
|
|
||||||
|
components: { UnitInput },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { terminationGracePeriodSeconds: this.value.value || this.value.default };
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
this.$set(this.value, 'value', String(this.terminationGracePeriodSeconds));
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.$set(this, 'terminationGracePeriodSeconds', Number(this.value.default));
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<UnitInput
|
||||||
|
v-model="terminationGracePeriodSeconds"
|
||||||
|
:suffix="terminationGracePeriodSeconds === 1 ? 'Second' : 'Seconds'"
|
||||||
|
:label="t('harvester.virtualMachine.terminationGracePeriodSeconds.label')"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
87
pkg/harvester/components/settings/http-proxy.vue
Normal file
87
pkg/harvester/components/settings/http-proxy.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHttpProxy',
|
||||||
|
|
||||||
|
components: { Banner, LabeledInput },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
const parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner color="warning">
|
||||||
|
<t k="harvester.setting.httpProxy.warning" :raw="true" />
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="row" @input="update">
|
||||||
|
<div class="col span-12">
|
||||||
|
<template>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.httpProxy"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
label="http-proxy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.httpsProxy"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
label="https-proxy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.noProxy"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
label="no-proxy"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
73
pkg/harvester/components/settings/ntp-servers.vue
Normal file
73
pkg/harvester/components/settings/ntp-servers.vue
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterNtpServersConfig',
|
||||||
|
|
||||||
|
components: { ArrayList },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = { ntpServers: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { parseDefaultValue };
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
let parseDefaultValue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
} catch (err) {
|
||||||
|
parseDefaultValue = { ntpServers: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
useDefault() {
|
||||||
|
const parseDefaultValue = { ntpServers: [] };
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<template>
|
||||||
|
<ArrayList
|
||||||
|
v-model="parseDefaultValue.ntpServers"
|
||||||
|
:title="t('harvester.host.ntp.label')"
|
||||||
|
:protip="t('harvester.host.ntp.tips')"
|
||||||
|
:value-placeholder="t('harvester.host.ntp.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
94
pkg/harvester/components/settings/overcommit-config.vue
Normal file
94
pkg/harvester/components/settings/overcommit-config.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterOvercommitConfig',
|
||||||
|
|
||||||
|
components: { UnitInput },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
const parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<template>
|
||||||
|
<UnitInput
|
||||||
|
v-model="parseDefaultValue.cpu"
|
||||||
|
v-int-number
|
||||||
|
label-key="harvester.generic.cpu"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UnitInput
|
||||||
|
v-model="parseDefaultValue.memory"
|
||||||
|
v-int-number
|
||||||
|
label-key="harvester.generic.memory"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UnitInput
|
||||||
|
v-model="parseDefaultValue.storage"
|
||||||
|
v-int-number
|
||||||
|
label-key="harvester.generic.storage"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
117
pkg/harvester/components/settings/ssl-certificates.vue
Normal file
117
pkg/harvester/components/settings/ssl-certificates.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import FileSelector from '@shell/components/form/FileSelector';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterSSLCertificates',
|
||||||
|
|
||||||
|
components: { FileSelector },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
caFileName: '',
|
||||||
|
publicCertificateFileName: '',
|
||||||
|
privateKeyFileName: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKeySelectedCa(type, file) {
|
||||||
|
const { name, value } = file;
|
||||||
|
|
||||||
|
this.$set(this.parseDefaultValue, type, value);
|
||||||
|
this.$set(this, `${ type }FileName`, name);
|
||||||
|
const _value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', _value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
const parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div class="mb-10">
|
||||||
|
{{ t('harvester.setting.sslCertificates.publicCertificate') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chooseFile">
|
||||||
|
<FileSelector
|
||||||
|
:include-file-name="true"
|
||||||
|
class="btn btn-sm bg-primary mr-20"
|
||||||
|
label="Choose File"
|
||||||
|
@selected="onKeySelectedCa('publicCertificate', $event)"
|
||||||
|
/>
|
||||||
|
<span :class="{ 'text-muted': !publicCertificateFileName }">{{ publicCertificateFileName ? publicCertificateFileName : t('harvester.generic.noFileChosen') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div class="mb-10">
|
||||||
|
{{ t('harvester.setting.sslCertificates.privateKey') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chooseFile">
|
||||||
|
<FileSelector
|
||||||
|
:include-file-name="true"
|
||||||
|
class="btn btn-sm bg-primary mr-20"
|
||||||
|
label="Choose File"
|
||||||
|
@selected="onKeySelectedCa('privateKey', $event)"
|
||||||
|
/>
|
||||||
|
<span :class="{ 'text-muted': !privateKeyFileName }">{{ privateKeyFileName ? privateKeyFileName : t('harvester.generic.noFileChosen') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div class="mb-10">
|
||||||
|
{{ t('harvester.setting.sslCertificates.ca') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chooseFile">
|
||||||
|
<FileSelector
|
||||||
|
:include-file-name="true"
|
||||||
|
class="btn btn-sm bg-primary mr-20"
|
||||||
|
label="Choose File"
|
||||||
|
@selected="onKeySelectedCa('ca', $event)"
|
||||||
|
/>
|
||||||
|
<span :class="{ 'text-muted': !caFileName }">{{ caFileName ? caFileName : t('harvester.generic.noFileChosen') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chooseFile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
161
pkg/harvester/components/settings/ssl-parameters.vue
Normal file
161
pkg/harvester/components/settings/ssl-parameters.vue
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterSslParameters',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parsedDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parsedDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocols = parsedDefaultValue.protocols && (parsedDefaultValue.protocols || '').split(' ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
parsedDefaultValue: {
|
||||||
|
protocols,
|
||||||
|
ciphers: parsedDefaultValue.ciphers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
protocolOptions() {
|
||||||
|
return [{
|
||||||
|
label: 'TLSv1.3',
|
||||||
|
value: 'TLSv1.3',
|
||||||
|
}, {
|
||||||
|
label: 'TLSv1.2',
|
||||||
|
value: 'TLSv1.2',
|
||||||
|
}, {
|
||||||
|
label: `TLSv1.1 (${ this.t('generic.deprecated') })`,
|
||||||
|
value: 'TLSv1.1',
|
||||||
|
}, {
|
||||||
|
label: `TLSv1 (${ this.t('generic.deprecated') })`,
|
||||||
|
value: 'TLSv1',
|
||||||
|
}, {
|
||||||
|
label: `SSLv3 (${ this.t('generic.deprecated') })`,
|
||||||
|
value: 'SSLv3',
|
||||||
|
}, {
|
||||||
|
label: `SSLv2 (${ this.t('generic.deprecated') })`,
|
||||||
|
value: 'SSLv2',
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const out = {
|
||||||
|
protocols: (this.parsedDefaultValue.protocols || []).join(' '),
|
||||||
|
ciphers: this.parsedDefaultValue.ciphers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = JSON.stringify(out);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
const ciphers = this.parsedDefaultValue.ciphers;
|
||||||
|
const protocols = this.parsedDefaultValue.protocols || [];
|
||||||
|
|
||||||
|
if (ciphers && protocols.length === 0) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.sslParameters.protocols.label') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ciphers && protocols.length > 0) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.sslParameters.ciphers.label') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /^(:?[A-Z0-9]+(?:-[A-Z0-9]+)+)+$/gm;
|
||||||
|
|
||||||
|
if (ciphers && (!ciphers.match(regex) || ciphers.startsWith(':'))) {
|
||||||
|
errors.push(this.t('validation.invalid', { key: this.t('harvester.sslParameters.ciphers.label') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'value.value': {
|
||||||
|
handler(value) {
|
||||||
|
if (value === this.value.default) {
|
||||||
|
this.parsedDefaultValue.protocols = [];
|
||||||
|
this.parsedDefaultValue.ciphers = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="parsedDefaultValue.protocols"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.sslParameters.protocols.label"
|
||||||
|
:multiple="true"
|
||||||
|
:options="protocolOptions"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parsedDefaultValue.ciphers"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.sslParameters.ciphers.label"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
240
pkg/harvester/components/settings/storage-network.vue
Normal file
240
pkg/harvester/components/settings/storage-network.vue
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import Tip from '@shell/components/Tip';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditStorageNetwork',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ArrayList,
|
||||||
|
Tip,
|
||||||
|
Banner,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parsedDefaultValue = {};
|
||||||
|
let openVlan = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedDefaultValue = JSON.parse(this.value.value);
|
||||||
|
openVlan = true;
|
||||||
|
} catch (error) {
|
||||||
|
parsedDefaultValue = {
|
||||||
|
vlan: '',
|
||||||
|
clusterNetwork: '',
|
||||||
|
range: '',
|
||||||
|
exclude: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
openVlan,
|
||||||
|
errors: [],
|
||||||
|
exclude,
|
||||||
|
parsedDefaultValue,
|
||||||
|
defaultAddValue: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
|
||||||
|
return clusterNetworks.map((n) => {
|
||||||
|
const disabled = !n.isReadyForStorageNetwork;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
|
||||||
|
value: n.id,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const exclude = this.exclude.filter(ip => ip);
|
||||||
|
|
||||||
|
if (Array.isArray(exclude) && exclude.length > 0) {
|
||||||
|
this.parsedDefaultValue.exclude = exclude;
|
||||||
|
} else {
|
||||||
|
delete this.parsedDefaultValue.exclude;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueString = JSON.stringify(this.parsedDefaultValue);
|
||||||
|
|
||||||
|
if (this.openVlan) {
|
||||||
|
this.$set(this.value, 'value', valueString);
|
||||||
|
} else {
|
||||||
|
this.$set(this.value, 'value', '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
this.update();
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (this.openVlan) {
|
||||||
|
const valid = !!/^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([1-9]|[1-2]\d|3[0-2])$/.test(this.parsedDefaultValue.range);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.parsedDefaultValue.vlan) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.parsedDefaultValue.clusterNetwork) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.exclude) {
|
||||||
|
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
||||||
|
return !isValidCIDR(cidr);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasInvalidCIDR) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="mode"
|
||||||
|
@input="update"
|
||||||
|
>
|
||||||
|
<Banner color="warning">
|
||||||
|
<t k="harvester.setting.storageNetwork.warning" :raw="true" />
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
v-model="openVlan"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="openVlan">
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="parsedDefaultValue.vlan"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="parsedDefaultValue.clusterNetwork"
|
||||||
|
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parsedDefaultValue.range"
|
||||||
|
class="mb-5"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
||||||
|
label-key="harvester.setting.storageNetwork.range.label"
|
||||||
|
/>
|
||||||
|
<Tip class="mb-20" icon="icon icon-info" :text="t('harvester.setting.storageNetwork.tip')">
|
||||||
|
<t k="harvester.setting.storageNetwork.tip" :raw="true" />
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
<ArrayList
|
||||||
|
v-model="exclude"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||||
|
@input="update"
|
||||||
|
>
|
||||||
|
<template v-slot:column-headers>
|
||||||
|
<div class="box">
|
||||||
|
<div class="key">
|
||||||
|
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
134
pkg/harvester/components/settings/support-bundle-image.vue
Normal file
134
pkg/harvester/components/settings/support-bundle-image.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script>
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterSupportBundleImage',
|
||||||
|
|
||||||
|
components: { LabeledInput, LabeledSelect },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { parseDefaultValue };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
imagePolicyOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('generic.imagePullPolicy.always'),
|
||||||
|
value: 'Always',
|
||||||
|
}, {
|
||||||
|
label: this.t('generic.imagePullPolicy.ifNotPresent'),
|
||||||
|
value: 'IfNotPresent',
|
||||||
|
}, {
|
||||||
|
label: this.t('generic.imagePullPolicy.never'),
|
||||||
|
value: 'Never'
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!this.parseDefaultValue.repository) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.supportBundleImage.repo') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.parseDefaultValue.tag) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.supportBundleImage.tag') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
const parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row" @input="update">
|
||||||
|
<div class="col span-12">
|
||||||
|
<template>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.repository"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.supportBundleImage.repo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="parseDefaultValue.tag"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.supportBundleImage.tag"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="parseDefaultValue.imagePullPolicy"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.supportBundleImage.imagePullPolicy"
|
||||||
|
:options="imagePolicyOptions"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
<script>
|
||||||
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterBundleNamespaces',
|
||||||
|
|
||||||
|
components: { LabeledSelect },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let namespaces = [];
|
||||||
|
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||||
|
|
||||||
|
if (namespacesStr) {
|
||||||
|
namespaces = namespacesStr.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { namespaces };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
namespaceOptions() {
|
||||||
|
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
||||||
|
return {
|
||||||
|
label: N.id,
|
||||||
|
value: N.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const namespaceStr = this.namespaces.join(',');
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', namespaceStr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'value.value': {
|
||||||
|
handler(neu) {
|
||||||
|
if (neu === this.value.default || !neu) {
|
||||||
|
this.namespaces = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="namespaces"
|
||||||
|
:multiple="true"
|
||||||
|
label-key="nameNsDescription.namespace.label"
|
||||||
|
:mode="mode"
|
||||||
|
:options="namespaceOptions"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
81
pkg/harvester/components/settings/vm-force-reset-policy.vue
Normal file
81
pkg/harvester/components/settings/vm-force-reset-policy.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVMForceDeletePolicy',
|
||||||
|
|
||||||
|
components: { LabeledInput, RadioGroup },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { parseDefaultValue };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
const value = JSON.stringify(this.parseDefaultValue);
|
||||||
|
|
||||||
|
this.$set(this.value, 'value', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
const parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
|
||||||
|
this.$set(this, 'parseDefaultValue', parseDefaultValue);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row" @input="update">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="parseDefaultValue.enable"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="parseDefaultValue.enable"
|
||||||
|
v-model.number="parseDefaultValue.period"
|
||||||
|
v-int-number
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
4
pkg/harvester/config/types.js
Normal file
4
pkg/harvester/config/types.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const BACKUP_TYPE = {
|
||||||
|
BACKUP: 'backup',
|
||||||
|
SNAPSHOT: 'snapshot'
|
||||||
|
};
|
||||||
331
pkg/harvester/detail/harvesterhci.io.host/HarvesterHostBasic.vue
Normal file
331
pkg/harvester/detail/harvesterhci.io.host/HarvesterHostBasic.vue
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { LONGHORN, METRIC } from '@shell/config/types';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
|
||||||
|
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
|
||||||
|
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
|
||||||
|
|
||||||
|
const COMPLETE = 'complete';
|
||||||
|
const PROMOTE_RESTART = 'promoteRestart';
|
||||||
|
const PROMOTE_SUCCEED = 'promoteSucceed';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BasicNode',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
LabelValue,
|
||||||
|
HarvesterCPUUsed,
|
||||||
|
HarvesterMemoryUsed,
|
||||||
|
HarvesterStorageUsed,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
metrics: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'view'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
customName() {
|
||||||
|
return this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME];
|
||||||
|
},
|
||||||
|
|
||||||
|
consoleUrl() {
|
||||||
|
const consoleUrl = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CONSOLE_URL];
|
||||||
|
let value = consoleUrl;
|
||||||
|
|
||||||
|
if (!consoleUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!consoleUrl.startsWith('http://') && !consoleUrl.startsWith('https://')) {
|
||||||
|
value = `http://${ consoleUrl }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
display: consoleUrl,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
cpuTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
if (this.metrics) {
|
||||||
|
out = this.metrics.cpuCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
cpuUsage() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
if (this.metrics) {
|
||||||
|
out = this.metrics.cpuUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
memoryTotal() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
if (this.metrics) {
|
||||||
|
out = this.metrics.memoryCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
memoryUsage() {
|
||||||
|
let out = 0;
|
||||||
|
|
||||||
|
if (this.metrics) {
|
||||||
|
out = this.metrics.memoryUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
cpuUnits() {
|
||||||
|
return 'C';
|
||||||
|
},
|
||||||
|
|
||||||
|
memoryUnits() {
|
||||||
|
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||||
|
|
||||||
|
return `${ UNITS[exponent] }iB`;
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeType() {
|
||||||
|
if (this.value.isEtcd) {
|
||||||
|
return this.t('harvester.host.detail.etcd');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.isMaster) {
|
||||||
|
return this.t('harvester.host.detail.management');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.t('harvester.host.detail.compute');
|
||||||
|
},
|
||||||
|
|
||||||
|
lastUpdateTime() {
|
||||||
|
return this.value.status?.conditions?.[0]?.lastHeartbeatTime;
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeRoleState() {
|
||||||
|
if (!this.value.isEtcd) {
|
||||||
|
const promoteStatus = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.PROMOTE_STATUS];
|
||||||
|
|
||||||
|
if (promoteStatus === COMPLETE) {
|
||||||
|
const isExistRoleStatus = this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_MASTER] !== undefined || this.value.metadata?.labels?.[HCI_ANNOTATIONS.NODE_ROLE_CONTROL_PLANE] !== undefined;
|
||||||
|
|
||||||
|
return this.t(`harvester.host.promote.${ isExistRoleStatus ? PROMOTE_SUCCEED : PROMOTE_RESTART }`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasMetricNodeSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](METRIC.NODE);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasLonghornSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](LONGHORN.NODES);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
memoryFormatter(value) {
|
||||||
|
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
addSuffix: false,
|
||||||
|
increment: 1024,
|
||||||
|
minExponent: exponent
|
||||||
|
};
|
||||||
|
|
||||||
|
return formatSi(value, formatOptions);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="host-detail">
|
||||||
|
<Banner
|
||||||
|
v-if="value.isKVMDisable"
|
||||||
|
color="error"
|
||||||
|
label-key="harvester.host.detail.kvm.disableMessage"
|
||||||
|
/>
|
||||||
|
<h3>{{ t('harvester.host.tabs.overview') }}</h3>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.customName')" :value="customName" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.hostIP')" :value="value.internalIp" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.os')" :value="value.status.nodeInfo.osImage" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<div class="role">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.role')">
|
||||||
|
<template #value>
|
||||||
|
{{ nodeType }}
|
||||||
|
<span
|
||||||
|
v-if="nodeRoleState"
|
||||||
|
class="text-warning ml-20"
|
||||||
|
>
|
||||||
|
{{ nodeRoleState }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.create')" :value="value.metadata.creationTimestamp" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.update')" :value="lastUpdateTime" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.consoleUrl')" :value="consoleUrl.value">
|
||||||
|
<a slot="value" :href="consoleUrl.value" target="_blank">{{ consoleUrl.display }}</a>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasMetricNodeSchema">
|
||||||
|
<hr class="divider" />
|
||||||
|
<h3>{{ t('harvester.host.tabs.monitor') }}</h3>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div
|
||||||
|
class="col"
|
||||||
|
:class="{
|
||||||
|
'span-4': hasLonghornSchema,
|
||||||
|
'span-6': !hasLonghornSchema,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<HarvesterCPUUsed
|
||||||
|
:row="value"
|
||||||
|
:resource-name="t('node.detail.glance.consumptionGauge.cpu')"
|
||||||
|
:show-used="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col"
|
||||||
|
:class="{
|
||||||
|
'span-4': hasLonghornSchema,
|
||||||
|
'span-6': !hasLonghornSchema,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<HarvesterMemoryUsed
|
||||||
|
:row="value"
|
||||||
|
:resource-name="t('node.detail.glance.consumptionGauge.memory')"
|
||||||
|
:show-used="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="hasLonghornSchema"
|
||||||
|
class="col span-4"
|
||||||
|
>
|
||||||
|
<HarvesterStorageUsed
|
||||||
|
:row="value"
|
||||||
|
:resource-name="t('harvester.host.detail.storage')"
|
||||||
|
:show-reserved="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-divider" />
|
||||||
|
<h3>{{ t('harvester.host.detail.more') }}</h3>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.uuid')" :value="value.status.nodeInfo.systemUUID" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.kernel')" :value="value.status.nodeInfo.kernelVersion" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.detail.containerRuntime')" :value="value.status.nodeInfo.containerRuntimeVersion" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="value.manufacturer || value.serialNumber || value.model"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="value.manufacturer"
|
||||||
|
class="col span-4"
|
||||||
|
>
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.detail.manufacturer')"
|
||||||
|
:value="value.manufacturer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="value.serialNumber"
|
||||||
|
class="col span-4"
|
||||||
|
>
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.detail.serialNumber')"
|
||||||
|
:value="value.serialNumber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="value.model"
|
||||||
|
class="col span-4"
|
||||||
|
>
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.detail.model')"
|
||||||
|
:value="value.model"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.role {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
214
pkg/harvester/detail/harvesterhci.io.host/HarvesterHostDisk.vue
Normal file
214
pkg/harvester/detail/harvesterhci.io.host/HarvesterHostDisk.vue
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<script>
|
||||||
|
import Tag from '@shell/components/Tag';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { BadgeState } from '@components/BadgeState';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LabelValue,
|
||||||
|
BadgeState,
|
||||||
|
Banner,
|
||||||
|
Tag
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [
|
||||||
|
HarvesterDisk,
|
||||||
|
],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disks: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'edit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
allowSchedulingOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('generic.enabled'),
|
||||||
|
value: true,
|
||||||
|
}, {
|
||||||
|
label: this.t('generic.disabled'),
|
||||||
|
value: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
evictionRequestedOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('generic.yes'),
|
||||||
|
value: true,
|
||||||
|
}, {
|
||||||
|
label: this.t('generic.no'),
|
||||||
|
value: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
provisionPhase() {
|
||||||
|
return this.value?.blockDevice?.provisionPhase || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
mountedMessage() {
|
||||||
|
const state = this.value?.blockDevice?.metadata?.state || {};
|
||||||
|
|
||||||
|
if (state?.error) {
|
||||||
|
return state?.message;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
this.$emit('input', this.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
canEditPath(value) {
|
||||||
|
if (this.mountedMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.isNew && !!value.originPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="disk" @input="update">
|
||||||
|
<Banner
|
||||||
|
v-if="mountedMessage"
|
||||||
|
color="error"
|
||||||
|
:label="mountedMessage"
|
||||||
|
/>
|
||||||
|
<div v-if="!value.isNew">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue
|
||||||
|
v-if="value.tags.length"
|
||||||
|
:name="t('harvester.host.disk.tags.label')"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
<div class="mt-5">
|
||||||
|
<Tag v-for="(prop, key) in value.tags" :key="key + prop" class="mr-5">
|
||||||
|
{{ prop }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div class="pull-right">
|
||||||
|
{{ t('harvester.host.disk.conditions') }}:
|
||||||
|
<BadgeState
|
||||||
|
v-clean-tooltip="readyCondition.message"
|
||||||
|
:color="readyCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||||
|
:icon="readyCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||||
|
label="Ready"
|
||||||
|
class="mr-10 ml-10 state"
|
||||||
|
/>
|
||||||
|
<BadgeState
|
||||||
|
v-clean-tooltip="schedulableCondition.message"
|
||||||
|
:color="schedulableCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||||
|
:icon="schedulableCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||||
|
label="Schedulable"
|
||||||
|
class="mr-10 state"
|
||||||
|
/>
|
||||||
|
<BadgeState
|
||||||
|
v-if="provisionPhase.label"
|
||||||
|
:color="provisionPhase.color"
|
||||||
|
:icon="provisionPhase.icon"
|
||||||
|
:label="provisionPhase.label"
|
||||||
|
class="mr-10 state"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!value.isNew" class="row mt-30">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.storageAvailable.label')"
|
||||||
|
:value="value.storageAvailable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.storageScheduled.label')"
|
||||||
|
:value="value.storageScheduled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.storageMaximum.label')"
|
||||||
|
:value="value.storageMaximum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="mt-10" />
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('generic.name')"
|
||||||
|
:value="value.displayName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.path.label')"
|
||||||
|
:value="value.path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.close {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding:0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disk {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.secret-name {
|
||||||
|
height: $input-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-state {
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
171
pkg/harvester/detail/harvesterhci.io.host/HarvesterKsmtuned.vue
Normal file
171
pkg/harvester/detail/harvesterhci.io.host/HarvesterKsmtuned.vue
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { ksmtunedMode, ksmtunedRunOption } from '../../edit/harvesterhci.io.host/HarvesterKsmtuned.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterKsmtuned',
|
||||||
|
components: { LabelValue },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.KSTUNED });
|
||||||
|
|
||||||
|
this.ksmtuned = hash.find((node) => {
|
||||||
|
return node.id === this.node.id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { ksmtuned: {} };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
modeText() {
|
||||||
|
const mode = this.ksmtuned.spec.mode;
|
||||||
|
|
||||||
|
return ksmtunedMode.find(M => M.value === mode).label;
|
||||||
|
},
|
||||||
|
|
||||||
|
thresCoef() {
|
||||||
|
return `${ this.ksmtuned.spec.thresCoef } %`;
|
||||||
|
},
|
||||||
|
|
||||||
|
runText() {
|
||||||
|
const run = this.ksmtuned.spec.run;
|
||||||
|
|
||||||
|
return ksmtunedRunOption.find(M => M.value === run).label;
|
||||||
|
},
|
||||||
|
|
||||||
|
showRunInformation() {
|
||||||
|
return this.ksmtuned.spec.run === 'run';
|
||||||
|
},
|
||||||
|
|
||||||
|
mergeNodesText() {
|
||||||
|
return this.ksmtuned.spec?.mergeAcrossNodes ? this.t('harvester.host.ksmtuned.enable') : this.t('harvester.host.ksmtuned.disable');
|
||||||
|
},
|
||||||
|
|
||||||
|
ksmdPhase() {
|
||||||
|
return this.ksmtuned?.status?.ksmdPhase;
|
||||||
|
},
|
||||||
|
|
||||||
|
ksmdPhaseTextColor() {
|
||||||
|
return this.ksmdPhase === 'Running' ? 'text-success' : 'text-warning';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="ksmtuned.status">
|
||||||
|
<h2>
|
||||||
|
{{ t('harvester.host.ksmtuned.configure') }}
|
||||||
|
</h2>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.run')" :value="runText" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showRunInformation" class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.thresCoef')" :value="thresCoef" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.mode')" :value="modeText" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.enableMergeNodes')" :value="mergeNodesText" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showRunInformation">
|
||||||
|
<hr class="divider" />
|
||||||
|
|
||||||
|
<h3>{{ t('harvester.host.ksmtuned.parameters.title') }}</h3>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.parameters.boost')" :value="ksmtuned.spec.ksmtunedParameters.boost" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.parameters.decay')" :value="ksmtuned.spec.ksmtunedParameters.decay" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.parameters.sleepMsec')" :value="ksmtuned.spec.ksmtunedParameters.sleepMsec" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.parameters.minPages')" :value="ksmtuned.spec.ksmtunedParameters.minPages" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.parameters.maxPages')" :value="ksmtuned.spec.ksmtunedParameters.maxPages" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr class="divider" />
|
||||||
|
<h3><t k="harvester.host.ksmtuned.statistics.title" :raw="true" /></h3>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.ksmStatus')">
|
||||||
|
<template #value>
|
||||||
|
<span :class="ksmdPhaseTextColor">{{ ksmdPhase }}</span>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.statistics.sharing')" :value="ksmtuned.status.sharing" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.statistics.shared')" :value="ksmtuned.status.shared" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.statistics.unshared')" :value="ksmtuned.status.unshared" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.statistics.volatile')" :value="ksmtuned.status.volatile" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.statistics.fullScans')" :value="ksmtuned.status.fullScans" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.statistics.stableNodeDups')" :value="ksmtuned.status.stableNodeDups" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue :name="t('harvester.host.ksmtuned.statistics.stableNodeChains')" :value="ksmtuned.status.stableNodeChains" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
131
pkg/harvester/detail/harvesterhci.io.host/HarvesterSeeder.vue
Normal file
131
pkg/harvester/detail/harvesterhci.io.host/HarvesterSeeder.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<script>
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterSeeder',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
LabelValue,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
inventory: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const enableInventory = !!this.inventory?.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
enableInventory,
|
||||||
|
value: this.inventory,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedSecret() {
|
||||||
|
const namespace = this.value.spec?.baseboardSpec?.connection?.authSecretRef?.namespace;
|
||||||
|
const name = this.value?.spec?.baseboardSpec?.connection?.authSecretRef?.name;
|
||||||
|
|
||||||
|
if (namespace && name) {
|
||||||
|
return `${ namespace }/${ name }`;
|
||||||
|
} else {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="inventory.warningMessages.length > 0">
|
||||||
|
<Banner
|
||||||
|
v-for="msg in inventory.warningMessages"
|
||||||
|
:key="msg.text"
|
||||||
|
color="error"
|
||||||
|
:label="msg.text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="enableInventory">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.seeder.inventory.host.label')"
|
||||||
|
:value="value.spec.baseboardSpec.connection.host"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.seeder.inventory.port.label')"
|
||||||
|
:value="value.spec.baseboardSpec.connection.port"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.seeder.inventory.insecureTLS.label')"
|
||||||
|
:value="value.spec.baseboardSpec.connection.insecureTLS ? t('generic.yes') : t('generic.no')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.seeder.inventory.secret.label')"
|
||||||
|
:value="selectedSecret"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.seeder.inventory.event.label')"
|
||||||
|
:value="value.spec.events.enabled ? t('generic.enabled') : t('generic.disabled')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="value.spec.events.enabled"
|
||||||
|
class="col span-6"
|
||||||
|
>
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.seeder.inventory.pollingInterval.label')"
|
||||||
|
:value="value.spec.events.pollingInterval"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="enableInventory"
|
||||||
|
:options="[
|
||||||
|
{ label: t('generic.enabled'), value: true },
|
||||||
|
{ label: t('generic.disabled'), value: false }
|
||||||
|
]"
|
||||||
|
:mode="mode"
|
||||||
|
name="enableInventory"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
<script>
|
||||||
|
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import HarvesterVmState from '../../formatters/HarvesterVmState';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { HOSTNAME } from '@shell/config/labels-annotations';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InstanceNode',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
SortableTable,
|
||||||
|
Loading,
|
||||||
|
HarvesterVmState,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const hash = await allHash({
|
||||||
|
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||||
|
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
||||||
|
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
});
|
||||||
|
const instanceMap = {};
|
||||||
|
|
||||||
|
(hash.vmis || []).forEach((vmi) => {
|
||||||
|
const vmiUID = vmi?.metadata?.ownerReferences?.[0]?.uid;
|
||||||
|
|
||||||
|
if (vmiUID) {
|
||||||
|
instanceMap[vmiUID] = vmi;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.allClusterNetwork = hash.allClusterNetwork;
|
||||||
|
this.rows = hash.vms.filter((row) => {
|
||||||
|
return instanceMap[row.metadata?.uid]?.status?.nodeName === this.node?.metadata?.labels?.[HOSTNAME];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
allClusterNetwork: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
STATE,
|
||||||
|
NAME,
|
||||||
|
{
|
||||||
|
name: 'vmCPU',
|
||||||
|
labelKey: 'tableHeaders.cpu',
|
||||||
|
search: false,
|
||||||
|
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||||
|
value: 'spec.template.spec.domain.cpu.cores',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vmRAM',
|
||||||
|
labelKey: 'glance.memory',
|
||||||
|
search: false,
|
||||||
|
sort: ['memorySort'],
|
||||||
|
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ip',
|
||||||
|
label: 'IP Address',
|
||||||
|
labelKey: 'harvester.tableHeaders.vm.ipAddress',
|
||||||
|
value: 'id',
|
||||||
|
formatter: 'HarvesterIpAddress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...AGE,
|
||||||
|
sort: 'metadata.creationTimestamp:desc',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else id="host-instances" class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<SortableTable
|
||||||
|
v-bind="$attrs"
|
||||||
|
:headers="headers"
|
||||||
|
default-sort-by="age"
|
||||||
|
:rows="rows"
|
||||||
|
key-field="_key"
|
||||||
|
v-on="$listeners"
|
||||||
|
>
|
||||||
|
<template slot="cell:state" slot-scope="scope" class="state-col">
|
||||||
|
<div class="state">
|
||||||
|
<HarvesterVmState class="vmstate" :row="scope.row" :all-cluster-network="allClusterNetwork" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Sortabletable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#host-instances {
|
||||||
|
::v-deep thead th {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .state {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.vmstate {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
|
||||||
|
import { _CREATE } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LinkStatus',
|
||||||
|
|
||||||
|
components: { LabelValue },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _CREATE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('generic.name')"
|
||||||
|
:value="value.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('tableHeaders.state')"
|
||||||
|
:value="value.state"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('tableHeaders.type')"
|
||||||
|
:value="value.type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.fields.macAddress')"
|
||||||
|
:value="value.mac"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div
|
||||||
|
v-if="value.promiscuous === 'true'"
|
||||||
|
class="col span-6"
|
||||||
|
>
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.fields.promiscuous')"
|
||||||
|
:value="value.promiscuous"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
{{ value.promiscuous === 'true' ? t('generic.yes') : t('generic.no') }}
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
104
pkg/harvester/detail/harvesterhci.io.host/VlanStatus/index.vue
Normal file
104
pkg/harvester/detail/harvesterhci.io.host/VlanStatus/index.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||||
|
import { BadgeState } from '@components/BadgeState';
|
||||||
|
|
||||||
|
import { _CREATE } from '@shell/config/query-params';
|
||||||
|
import { findBy } from '@shell/utils/array';
|
||||||
|
|
||||||
|
import LinkStatus from './LinkStatus';
|
||||||
|
import { HCI } from '../../../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHostNetwork',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabelValue,
|
||||||
|
LinkStatus,
|
||||||
|
ArrayListGrouped,
|
||||||
|
BadgeState,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _CREATE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
conditions() {
|
||||||
|
return this.value?.status?.conditions || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
readyCondition() {
|
||||||
|
return findBy(this.conditions, 'type', 'ready') || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
linkStatus() {
|
||||||
|
const linkMonitorId = this.value?.status?.linkMonitor;
|
||||||
|
const nodeName = this.value?.status?.node;
|
||||||
|
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const linkMonitors = this.$store.getters[`${ inStore }/all`](HCI.LINK_MONITOR);
|
||||||
|
const linkMonitor = (linkMonitors.filter(l => l.id === linkMonitorId) || [])[0] || {};
|
||||||
|
|
||||||
|
return linkMonitor?.status?.linkStatus?.[nodeName] || [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div class="pull-right">
|
||||||
|
{{ t('resourceTabs.conditions.tab') }}:
|
||||||
|
<BadgeState
|
||||||
|
v-clean-tooltip="readyCondition.message"
|
||||||
|
:color="readyCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||||
|
:icon="readyCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||||
|
:label="t('tableHeaders.ready')"
|
||||||
|
class="mr-10 ml-10 state"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.network.clusterNetwork.label')"
|
||||||
|
:value="value.status.clusterNetwork"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.vlanStatus.vlanConfig.label')"
|
||||||
|
:value="value.status.vlanConfig"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<ArrayListGrouped
|
||||||
|
v-model="linkStatus"
|
||||||
|
:mode="mode"
|
||||||
|
:can-remove="false"
|
||||||
|
>
|
||||||
|
<template #default="props">
|
||||||
|
<LinkStatus
|
||||||
|
:value="props.row.value"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ArrayListGrouped>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
488
pkg/harvester/detail/harvesterhci.io.host/index.vue
Normal file
488
pkg/harvester/detail/harvesterhci.io.host/index.vue
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Tag from '@shell/components/Tag';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||||
|
import Loading from '@shell/components/Loading.vue';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
|
|
||||||
|
import metricPoller from '@shell/mixins/metric-poller';
|
||||||
|
import {
|
||||||
|
METRIC, NODE, LONGHORN, POD, EVENT
|
||||||
|
} from '@shell/config/types';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { formatSi } from '@shell/utils/units';
|
||||||
|
import { findBy } from '@shell/utils/array';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
|
||||||
|
import Basic from './HarvesterHostBasic';
|
||||||
|
import Instance from './VirtualMachineInstance';
|
||||||
|
import Disk from './HarvesterHostDisk';
|
||||||
|
import VlanStatus from './VlanStatus';
|
||||||
|
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||||
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
|
|
||||||
|
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DetailHost',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
Tag,
|
||||||
|
Basic,
|
||||||
|
Instance,
|
||||||
|
ArrayListGrouped,
|
||||||
|
Disk,
|
||||||
|
InfoBox,
|
||||||
|
VlanStatus,
|
||||||
|
LabelValue,
|
||||||
|
HarvesterKsmtuned,
|
||||||
|
Loading,
|
||||||
|
SortableTable,
|
||||||
|
HarvesterSeeder,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
mixins: [metricPoller],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
nodes: this.$store.dispatch('harvester/findAll', { type: NODE }),
|
||||||
|
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.VLAN_STATUS)) {
|
||||||
|
hash.hostNetworks = this.$store.dispatch('harvester/findAll', { type: HCI.VLAN_STATUS });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE)) {
|
||||||
|
hash.blockDevices = this.$store.dispatch('harvester/findAll', { type: HCI.BLOCK_DEVICE });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](LONGHORN.NODES)) {
|
||||||
|
hash.longhornNodes = this.$store.dispatch('harvester/findAll', { type: LONGHORN.NODES });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.LINK_MONITOR)) {
|
||||||
|
hash.linkMonitors = this.$store.dispatch('harvester/findAll', { type: HCI.LINK_MONITOR });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.ADD_ONS)) {
|
||||||
|
hash.addons = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.getters['harvester/schemaFor'](HCI.INVENTORY)) {
|
||||||
|
hash.inventories = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.INVENTORY });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await allHash(hash);
|
||||||
|
const hostNetworkResource = (res.hostNetworks || []).find( O => this.value.id === O.attachNodeName);
|
||||||
|
|
||||||
|
this.loadMetrics();
|
||||||
|
|
||||||
|
if (hostNetworkResource) {
|
||||||
|
this.hostNetworkResource = hostNetworkResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
||||||
|
const provisionedBlockDevices = blockDevices.filter((d) => {
|
||||||
|
const provisioned = d?.spec?.fileSystem?.provisioned;
|
||||||
|
const isCurrentNode = d?.spec?.nodeName === this.value.id;
|
||||||
|
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
|
||||||
|
|
||||||
|
return provisioned && isCurrentNode && !isLonghornMounted;
|
||||||
|
})
|
||||||
|
.map((d) => {
|
||||||
|
return {
|
||||||
|
isNew: true,
|
||||||
|
name: d?.metadata?.name,
|
||||||
|
originPath: d?.spec?.fileSystem?.mountPoint,
|
||||||
|
path: d?.spec?.fileSystem?.mountPoint,
|
||||||
|
blockDevice: d,
|
||||||
|
displayName: d?.displayName,
|
||||||
|
forceFormatted: d?.spec?.fileSystem?.forceFormatted || false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const disks = [...this.longhornDisks, ...provisionedBlockDevices];
|
||||||
|
|
||||||
|
this.disks = disks;
|
||||||
|
this.newDisks = clone(disks);
|
||||||
|
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||||
|
const seeder = addons.find(addon => addon.id === 'harvester-system/harvester-seeder');
|
||||||
|
|
||||||
|
const seederEnabled = seeder ? seeder?.spec?.enabled : false;
|
||||||
|
|
||||||
|
if (seederEnabled) {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const inventories = this.$store.getters[`${ inStore }/all`](HCI.INVENTORY) || [];
|
||||||
|
|
||||||
|
const inventory = inventories.find(inv => inv.id === `harvester-system/${ this.value.id }`);
|
||||||
|
|
||||||
|
if (inventory) {
|
||||||
|
this.inventory = inventory;
|
||||||
|
} else {
|
||||||
|
this.inventory = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.INVENTORY,
|
||||||
|
metadata: {
|
||||||
|
name: this.value.id,
|
||||||
|
namespace: 'harvester-system'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inventory.applyDefaults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
metrics: null,
|
||||||
|
mode: 'view',
|
||||||
|
hostNetworkResource: null,
|
||||||
|
newDisks: [],
|
||||||
|
disks: [],
|
||||||
|
allEvents: [],
|
||||||
|
didLoadEvents: false,
|
||||||
|
inventory: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
longhornDisks() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `longhorn-system/${ this.value.id }`);
|
||||||
|
const diskStatus = longhornNode?.status?.diskStatus || {};
|
||||||
|
const diskSpec = longhornNode?.spec?.disks || {};
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
increment: 1024,
|
||||||
|
minExponent: 3,
|
||||||
|
maxExponent: 3,
|
||||||
|
maxPrecision: 2,
|
||||||
|
suffix: 'iB',
|
||||||
|
};
|
||||||
|
|
||||||
|
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||||
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `longhorn-system/${ key }`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...diskStatus[key],
|
||||||
|
...diskSpec?.[key],
|
||||||
|
name: key,
|
||||||
|
isNew: false,
|
||||||
|
storageReserved: formatSi(diskSpec[key]?.storageReserved, formatOptions),
|
||||||
|
storageAvailable: formatSi(diskStatus[key]?.storageAvailable, formatOptions),
|
||||||
|
storageMaximum: formatSi(diskStatus[key]?.storageMaximum, formatOptions),
|
||||||
|
storageScheduled: formatSi(diskStatus[key]?.storageScheduled, formatOptions),
|
||||||
|
blockDevice,
|
||||||
|
displayName: blockDevice?.displayName || key,
|
||||||
|
forceFormatted: blockDevice?.spec?.fileSystem?.forceFormatted || false,
|
||||||
|
tags: diskSpec?.[key]?.tags || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return longhornDisks;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasKsmtunedSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasBlockDevicesSchema() {
|
||||||
|
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasHostNetworksSchema() {
|
||||||
|
return !!this.$store.getters['harvester/schemaFor'](HCI.VLAN_STATUS);
|
||||||
|
},
|
||||||
|
|
||||||
|
vlanStatuses() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const nodeId = this.value.id;
|
||||||
|
const vlanStatuses = this.$store.getters[`${ inStore }/all`](HCI.VLAN_STATUS);
|
||||||
|
|
||||||
|
return vlanStatuses.filter(s => s?.status?.node === nodeId) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
longhornNode() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const longhornNodes = this.$store.getters[`${ inStore }/all`](LONGHORN.NODES);
|
||||||
|
|
||||||
|
return longhornNodes.find(node => node.id === `${ LONGHORN_SYSTEM }/${ this.value.id }`);
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return this.allEvents.filter((event) => {
|
||||||
|
return event.involvedObject?.uid === this.value?.metadata?.uid &&
|
||||||
|
event.reason !== 'SeederUpdated';
|
||||||
|
}).map((event) => {
|
||||||
|
return {
|
||||||
|
reason: (`${ event.reason || this.t('generic.unknown') }${ event.count > 1 ? ` (${ event.count })` : '' }`).trim(),
|
||||||
|
message: event.message || this.t('generic.unknown'),
|
||||||
|
date: event.lastTimestamp || event.firstTimestamp || event.metadata.creationTimestamp,
|
||||||
|
eventType: event.eventType
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
eventHeaders() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'reason',
|
||||||
|
label: this.t('tableHeaders.reason'),
|
||||||
|
value: 'reason',
|
||||||
|
sort: 'reason',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
label: this.t('tableHeaders.message'),
|
||||||
|
value: 'message',
|
||||||
|
sort: 'message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
label: this.t('tableHeaders.updated'),
|
||||||
|
value: 'date',
|
||||||
|
sort: 'date:desc',
|
||||||
|
formatter: 'LiveDate',
|
||||||
|
formatterOpts: { addSuffix: true },
|
||||||
|
width: 125
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
seederEnabled() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||||
|
const seeder = addons.find(addon => addon.id === 'harvester-system/harvester-seeder');
|
||||||
|
|
||||||
|
return seeder ? seeder?.spec?.enabled : false;
|
||||||
|
},
|
||||||
|
|
||||||
|
ntpSync() {
|
||||||
|
const jsonString = this.value.metadata?.annotations?.[HCI_ANNOTATIONS.NODE_NTP_SYNC_STATUS];
|
||||||
|
let out = null;
|
||||||
|
|
||||||
|
if (!jsonString) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
out = JSON.parse(jsonString);
|
||||||
|
} catch (err) {
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(this.value.metadata.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
ntpSyncedStatus() {
|
||||||
|
const status = this.ntpSync?.ntpSyncStatus;
|
||||||
|
|
||||||
|
if (status === 'disabled') {
|
||||||
|
return {
|
||||||
|
status: 'disabled',
|
||||||
|
warning: { key: 'harvester.host.ntp.ntpSyncStatus.isDisabled' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = this.ntpSync?.currentNtpServers || '';
|
||||||
|
|
||||||
|
if (status === 'unsynced') {
|
||||||
|
return {
|
||||||
|
status: 'unsynced',
|
||||||
|
warning: {
|
||||||
|
key: 'harvester.host.ntp.ntpSyncStatus.isUnsynced',
|
||||||
|
current
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadMetrics() {
|
||||||
|
const schema = this.$store.getters['harvester/schemaFor'](METRIC.NODE);
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
this.metrics = await this.$store.dispatch('harvester/find', {
|
||||||
|
type: METRIC.NODE,
|
||||||
|
id: this.value.id,
|
||||||
|
opt: { force: true, watch: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$forceUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ensures we only fetch events and show the table when the events tab has been activated
|
||||||
|
tabChange(neu) {
|
||||||
|
if (!this.didLoadEvents && neu?.selectedName === 'events') {
|
||||||
|
this.$store.dispatch(`harvester/findAll`, { type: EVENT }).then((events) => {
|
||||||
|
this.allEvents = events;
|
||||||
|
this.didLoadEvents = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else>
|
||||||
|
<Banner
|
||||||
|
v-if="ntpSyncedStatus.status === 'disabled'"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<span v-clean-html="t(ntpSyncedStatus.warning.key)"></span>
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-if="ntpSyncedStatus.status === 'unsynced'"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<span v-clean-html="t(ntpSyncedStatus.warning.key, { current: ntpSyncedStatus.warning.current }, true)"></span>
|
||||||
|
</Banner>
|
||||||
|
<Tabbed
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="mt-15"
|
||||||
|
:side-tabs="true"
|
||||||
|
@changed="tabChange"
|
||||||
|
>
|
||||||
|
<Tab name="basics" :label="t('harvester.host.tabs.basics')" :weight="4" class="bordered-table">
|
||||||
|
<Basic
|
||||||
|
v-model="value"
|
||||||
|
:metrics="metrics"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab name="instance" :label="t('harvester.host.tabs.instance')" :weight="3" class="bordered-table">
|
||||||
|
<Instance :node="value" />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="hasHostNetworksSchema && vlanStatuses.length > 0"
|
||||||
|
name="network"
|
||||||
|
:label="t('harvester.host.tabs.network')"
|
||||||
|
:weight="2"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<InfoBox
|
||||||
|
v-for="vlan in vlanStatuses"
|
||||||
|
:key="vlan.id"
|
||||||
|
>
|
||||||
|
<VlanStatus
|
||||||
|
:value="vlan"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</InfoBox>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="hasBlockDevicesSchema"
|
||||||
|
name="disk"
|
||||||
|
:weight="1"
|
||||||
|
:label="t('harvester.host.tabs.storage')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="longhornNode"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue
|
||||||
|
v-if="longhornNode.spec.tags.length"
|
||||||
|
:name="t('harvester.host.tags.label')"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
<div class="mt-5">
|
||||||
|
<Tag v-for="(prop, key) in longhornNode.spec.tags" :key="key + prop" class="mr-5">
|
||||||
|
{{ prop }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrayListGrouped
|
||||||
|
v-model="newDisks"
|
||||||
|
:mode="mode"
|
||||||
|
:can-remove="false"
|
||||||
|
:initial-empty-row="false"
|
||||||
|
>
|
||||||
|
<template #default="props">
|
||||||
|
<Disk
|
||||||
|
v-model="props.row.value"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:disks="disks"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ArrayListGrouped>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="hasKsmtunedSchema"
|
||||||
|
name="ksmtuned"
|
||||||
|
:weight="0"
|
||||||
|
:show-header="false"
|
||||||
|
:label="t('harvester.host.tabs.ksmtuned')"
|
||||||
|
>
|
||||||
|
<HarvesterKsmtuned :mode="mode" :node="value" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="seederEnabled"
|
||||||
|
name="seeder"
|
||||||
|
:weight="-1"
|
||||||
|
:label="t('harvester.host.tabs.seeder')"
|
||||||
|
>
|
||||||
|
<HarvesterSeeder
|
||||||
|
:mode="mode"
|
||||||
|
:node="value"
|
||||||
|
:inventory="inventory"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
label-key="harvester.virtualMachine.detail.tabs.events"
|
||||||
|
name="events"
|
||||||
|
:weight="-99"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:rows="events"
|
||||||
|
:headers="eventHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:search="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
default-sort-by="date"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
287
pkg/harvester/detail/harvesterhci.io.secret.vue
Normal file
287
pkg/harvester/detail/harvesterhci.io.secret.vue
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
<script>
|
||||||
|
import { SECRET_TYPES as TYPES } from '@shell/config/secret';
|
||||||
|
import { base64Decode } from '@shell/utils/crypto';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import DetailText from '@shell/components/DetailText';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
TYPES.OPAQUE,
|
||||||
|
TYPES.DOCKER_JSON,
|
||||||
|
TYPES.TLS,
|
||||||
|
TYPES.SSH,
|
||||||
|
TYPES.BASIC,
|
||||||
|
];
|
||||||
|
const registryAddresses = [
|
||||||
|
'DockerHub', 'Quay.io', 'Artifactory', 'Custom'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ResourceTabs,
|
||||||
|
DetailText,
|
||||||
|
Tab,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let username;
|
||||||
|
let password;
|
||||||
|
let registryUrl;
|
||||||
|
let registryProvider = 'Custom';
|
||||||
|
let key;
|
||||||
|
let crt;
|
||||||
|
|
||||||
|
if (this.value._type === TYPES.DOCKER_JSON) {
|
||||||
|
const json = base64Decode(this.value.data['.dockerconfigjson']);
|
||||||
|
|
||||||
|
const { auths } = JSON.parse(json);
|
||||||
|
|
||||||
|
registryUrl = Object.keys(auths)[0];
|
||||||
|
|
||||||
|
if (registryUrl === 'index.docker.io/v1/') {
|
||||||
|
registryProvider = 'DockerHub';
|
||||||
|
} else if (registryUrl === 'quay.io') {
|
||||||
|
registryProvider = 'Quay.io';
|
||||||
|
} else if (registryUrl.includes('artifactory')) {
|
||||||
|
registryProvider = 'Artifactory';
|
||||||
|
}
|
||||||
|
|
||||||
|
username = auths[registryUrl].username;
|
||||||
|
password = auths[registryUrl].password;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.value?.data || {};
|
||||||
|
|
||||||
|
if (this.value._type === TYPES.TLS) {
|
||||||
|
// do not show existing key when editing
|
||||||
|
key = this.mode === 'edit' ? '' : base64Decode(data['tls.key']);
|
||||||
|
|
||||||
|
crt = base64Decode(data['tls.crt']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value._type === TYPES.SERVICE_ACCT) {
|
||||||
|
key = base64Decode(data['token']);
|
||||||
|
crt = base64Decode(data['ca.crt']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.value._type === TYPES.BASIC ) {
|
||||||
|
username = base64Decode(data.username || '');
|
||||||
|
password = base64Decode(data.password || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.value._type === TYPES.SSH ) {
|
||||||
|
username = base64Decode(data['ssh-publickey'] || '');
|
||||||
|
password = base64Decode(data['ssh-privatekey'] || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.value._type) {
|
||||||
|
this.$set(this.value, '_type', TYPES.OPAQUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
types,
|
||||||
|
registryAddresses,
|
||||||
|
registryProvider,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
registryUrl,
|
||||||
|
key,
|
||||||
|
crt,
|
||||||
|
relatedServices: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isCertificate() {
|
||||||
|
return this.value._type === TYPES.TLS;
|
||||||
|
},
|
||||||
|
|
||||||
|
isSvcAcctToken() {
|
||||||
|
return this.value._type === TYPES.SERVICE_ACCT;
|
||||||
|
},
|
||||||
|
|
||||||
|
isRegistry() {
|
||||||
|
return this.value._type === TYPES.DOCKER_JSON;
|
||||||
|
},
|
||||||
|
|
||||||
|
isSsh() {
|
||||||
|
return this.value._type === TYPES.SSH;
|
||||||
|
},
|
||||||
|
|
||||||
|
isBasicAuth() {
|
||||||
|
return this.value._type === TYPES.BASIC;
|
||||||
|
},
|
||||||
|
|
||||||
|
parsedRows() {
|
||||||
|
const rows = [];
|
||||||
|
const { data = {} } = this.value;
|
||||||
|
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
const value = base64Decode(data[key]);
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
dataLabel() {
|
||||||
|
switch (this.value._type) {
|
||||||
|
case TYPES.TLS:
|
||||||
|
return this.t('secret.certificate.certificate');
|
||||||
|
case TYPES.SSH:
|
||||||
|
return this.t('secret.ssh.keys');
|
||||||
|
case TYPES.BASIC:
|
||||||
|
return this.t('secret.authentication');
|
||||||
|
default:
|
||||||
|
return this.t('secret.data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ResourceTabs
|
||||||
|
v-model="value"
|
||||||
|
:need-events="false"
|
||||||
|
:need-related="false"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="data"
|
||||||
|
:label="dataLabel"
|
||||||
|
>
|
||||||
|
<template v-if="isRegistry || isBasicAuth">
|
||||||
|
<div
|
||||||
|
v-if="isRegistry"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<DetailText
|
||||||
|
:value="registryUrl"
|
||||||
|
label-key="secret.registry.domainName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="username"
|
||||||
|
label-key="secret.registry.username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="password"
|
||||||
|
label-key="secret.registry.password"
|
||||||
|
:conceal="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="isCertificate"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="key"
|
||||||
|
label-key="secret.certificate.privateKey"
|
||||||
|
:conceal="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="crt"
|
||||||
|
label-key="secret.certificate.certificate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="isSvcAcctToken"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="crt"
|
||||||
|
label-key="secret.serviceAcct.ca"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="key"
|
||||||
|
label-key="secret.serviceAcct.token"
|
||||||
|
:conceal="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="isSsh"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="username"
|
||||||
|
label-key="secret.ssh.public"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<DetailText
|
||||||
|
:value="password"
|
||||||
|
label-key="secret.ssh.private"
|
||||||
|
:conceal="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="(row,idx) in parsedRows"
|
||||||
|
:key="idx"
|
||||||
|
class="entry"
|
||||||
|
>
|
||||||
|
<DetailText
|
||||||
|
:value="row.value"
|
||||||
|
:label="row.key"
|
||||||
|
:conceal="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="!parsedRows.length">
|
||||||
|
<div
|
||||||
|
v-t="'sortableTable.noRows'"
|
||||||
|
class="m-20 text-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.entry {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import CpuMemory from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
||||||
|
|
||||||
|
import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineKeypairs';
|
||||||
|
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
|
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
|
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BackupDetail',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Volume,
|
||||||
|
Network,
|
||||||
|
CruResource,
|
||||||
|
Tabbed,
|
||||||
|
Loading,
|
||||||
|
LabelValue,
|
||||||
|
Tab,
|
||||||
|
CloudConfig,
|
||||||
|
Checkbox,
|
||||||
|
CpuMemory,
|
||||||
|
OverviewKeypairs,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await allHash({ allImages: this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE }) });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { vm: null };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
return this.value?.metadata?.name || UNDEFINED;
|
||||||
|
},
|
||||||
|
|
||||||
|
hostname() {
|
||||||
|
return this?.spec?.template?.spec?.hostname;
|
||||||
|
},
|
||||||
|
|
||||||
|
imageName() {
|
||||||
|
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
|
||||||
|
|
||||||
|
const image = imageList.find( I => this.imageId === I.id);
|
||||||
|
|
||||||
|
return image?.spec?.displayName || '-';
|
||||||
|
},
|
||||||
|
|
||||||
|
disks() {
|
||||||
|
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||||
|
|
||||||
|
return disks.filter((disk) => {
|
||||||
|
return !!disk.bootOrder;
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (a.bootOrder < b.bootOrder) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cdroms() {
|
||||||
|
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||||
|
|
||||||
|
return disks.filter((disk) => {
|
||||||
|
return !!disk.cdrom;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getDeviceType(o) {
|
||||||
|
if (o.disk) {
|
||||||
|
return 'Disk';
|
||||||
|
} else {
|
||||||
|
return 'CD-ROM';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEmpty(o) {
|
||||||
|
return o !== undefined && Object.keys(o).length === 0;
|
||||||
|
},
|
||||||
|
onTabChanged({ tab }) {
|
||||||
|
if (tab.name === 'advanced') {
|
||||||
|
this.$refs.yamlEditor?.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
>
|
||||||
|
<Tabbed v-if="spec" :side-tabs="true" @changed="onTabChanged">
|
||||||
|
<Tab name="Basics" :label="t('harvester.virtualMachine.detail.tabs.basics')">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.bootOrder')">
|
||||||
|
<template #value>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(disk) in disks" :key="disk.bootOrder">
|
||||||
|
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.CDROMs')">
|
||||||
|
<template #value>
|
||||||
|
<div>
|
||||||
|
<ul v-if="cdroms.length > 0">
|
||||||
|
<li v-for="(rom) in cdroms" :key="rom.name">
|
||||||
|
{{ rom.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span v-else>
|
||||||
|
{{ t("harvester.virtualMachine.detail.notAvailable") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="volume"
|
||||||
|
:label="t('harvester.tab.volume')"
|
||||||
|
:weight="-1"
|
||||||
|
>
|
||||||
|
<Volume v-model="diskRows" :mode="mode" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="network"
|
||||||
|
:label="t('harvester.tab.network')"
|
||||||
|
:weight="-2"
|
||||||
|
>
|
||||||
|
<Network v-model="networkRows" :mode="mode" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="-3">
|
||||||
|
<OverviewKeypairs v-if="vm" v-model="vm" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="advanced"
|
||||||
|
:label="t('harvester.tab.advanced')"
|
||||||
|
:weight="-4"
|
||||||
|
>
|
||||||
|
<CloudConfig
|
||||||
|
ref="yamlEditor"
|
||||||
|
:user-script="userScript"
|
||||||
|
:mode="mode"
|
||||||
|
:network-script="networkScript"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<Checkbox v-model="installUSBTablet" :mode="mode" class="check" type="checkbox" :label="t('harvester.virtualMachine.enableUsb')" />
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: { LabelValue },
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
migratable() {
|
||||||
|
return this.value.migratable === 'true' ? this.t('generic.yes') : this.t('generic.no');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="value.nodeSelector"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.storage.nodeSelector.label')"
|
||||||
|
:value="value.nodeSelector"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="value.diskSelector"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.storage.diskSelector.label')"
|
||||||
|
:value="value.diskSelector"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.storage.migratable.label')"
|
||||||
|
:value="migratable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.storage.numberOfReplicas.label')"
|
||||||
|
:value="value.numberOfReplicas"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.storage.staleReplicaTimeout.label')"
|
||||||
|
:value="value.staleReplicaTimeout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
<script>
|
||||||
|
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { DESCRIPTION } from '@shell/config/labels-annotations';
|
||||||
|
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { findBy } from '@shell/utils/array';
|
||||||
|
import { get } from '@shell/utils/object';
|
||||||
|
|
||||||
|
import Storage from './Storage';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CopyToClipboardText,
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
LabelValue,
|
||||||
|
Storage,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
formattedValue() {
|
||||||
|
return this.value?.downSize;
|
||||||
|
},
|
||||||
|
|
||||||
|
url() {
|
||||||
|
return this.value?.spec?.url || '-';
|
||||||
|
},
|
||||||
|
|
||||||
|
description() {
|
||||||
|
return this.value?.metadata?.annotations?.[DESCRIPTION] || '-';
|
||||||
|
},
|
||||||
|
|
||||||
|
errorMessage() {
|
||||||
|
const conditions = get(this.value, 'status.conditions');
|
||||||
|
|
||||||
|
return findBy(conditions, 'type', 'Imported')?.message || '-';
|
||||||
|
},
|
||||||
|
|
||||||
|
isUpload() {
|
||||||
|
return this.value?.spec?.sourceType === 'upload';
|
||||||
|
},
|
||||||
|
|
||||||
|
imageName() {
|
||||||
|
return this.value?.metadata?.annotations?.[HCI.IMAGE_NAME] || '-';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="detail"
|
||||||
|
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
||||||
|
class="bordered-table"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue
|
||||||
|
v-if="isUpload"
|
||||||
|
:name="t('harvester.image.fileName')"
|
||||||
|
:value="imageName"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
<LabelValue
|
||||||
|
v-else
|
||||||
|
:name="t('harvester.image.url')"
|
||||||
|
:value="url"
|
||||||
|
class="mb-20"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
<div v-if="url !== '-'">
|
||||||
|
<CopyToClipboardText :text="url" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ url }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue :name="t('harvester.image.size')" :value="formattedValue" class="mb-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue :name="t('nameNsDescription.description.label')" :value="description" class="mb-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMessage !== '-'" class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div>
|
||||||
|
{{ t('tableHeaders.message') }}
|
||||||
|
</div>
|
||||||
|
<div :class="{ 'error': errorMessage !== '-' }">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="storage"
|
||||||
|
:label="t('harvester.storage.label')"
|
||||||
|
:weight="89"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<Storage
|
||||||
|
v-model="value.spec.storageClassParameters"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.error {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
221
pkg/harvester/detail/harvesterhci.io.vmsnapshot/index.vue
Normal file
221
pkg/harvester/detail/harvesterhci.io.vmsnapshot/index.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import CpuMemory from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
|
||||||
|
|
||||||
|
import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineKeypairs';
|
||||||
|
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
|
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
|
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMSnapshotDetail',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Volume,
|
||||||
|
Network,
|
||||||
|
CruResource,
|
||||||
|
Tabbed,
|
||||||
|
Loading,
|
||||||
|
LabelValue,
|
||||||
|
Tab,
|
||||||
|
CloudConfig,
|
||||||
|
Checkbox,
|
||||||
|
CpuMemory,
|
||||||
|
OverviewKeypairs,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await allHash({ allImages: this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE }) });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { vm: null };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
return this.value?.metadata?.name || UNDEFINED;
|
||||||
|
},
|
||||||
|
|
||||||
|
hostname() {
|
||||||
|
return this?.spec?.template?.spec?.hostname;
|
||||||
|
},
|
||||||
|
|
||||||
|
imageName() {
|
||||||
|
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
|
||||||
|
|
||||||
|
const image = imageList.find( I => this.imageId === I.id);
|
||||||
|
|
||||||
|
return image?.spec?.displayName || '-';
|
||||||
|
},
|
||||||
|
|
||||||
|
disks() {
|
||||||
|
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||||
|
|
||||||
|
return disks.filter((disk) => {
|
||||||
|
return !!disk.bootOrder;
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (a.bootOrder < b.bootOrder) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cdroms() {
|
||||||
|
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||||
|
|
||||||
|
return disks.filter((disk) => {
|
||||||
|
return !!disk.cdrom;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getDeviceType(o) {
|
||||||
|
if (o.disk) {
|
||||||
|
return 'Disk';
|
||||||
|
} else {
|
||||||
|
return 'CD-ROM';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEmpty(o) {
|
||||||
|
return o !== undefined && Object.keys(o).length === 0;
|
||||||
|
},
|
||||||
|
onTabChanged({ tab }) {
|
||||||
|
if (tab.name === 'advanced') {
|
||||||
|
this.$refs.yamlEditor?.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
>
|
||||||
|
<Tabbed v-if="spec" :side-tabs="true" @changed="onTabChanged">
|
||||||
|
<Tab name="Basics" :label="t('harvester.virtualMachine.detail.tabs.basics')">
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
|
||||||
|
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.bootOrder')">
|
||||||
|
<template #value>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(disk) in disks" :key="disk.bootOrder">
|
||||||
|
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.CDROMs')">
|
||||||
|
<template #value>
|
||||||
|
<div>
|
||||||
|
<ul v-if="cdroms.length > 0">
|
||||||
|
<li v-for="(rom) in cdroms" :key="rom.name">
|
||||||
|
{{ rom.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span v-else>
|
||||||
|
{{ t("harvester.virtualMachine.detail.notAvailable") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="volume"
|
||||||
|
:label="t('harvester.tab.volume')"
|
||||||
|
:weight="-1"
|
||||||
|
>
|
||||||
|
<Volume v-model="diskRows" :mode="mode" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="network"
|
||||||
|
:label="t('harvester.tab.network')"
|
||||||
|
:weight="-2"
|
||||||
|
>
|
||||||
|
<Network v-model="networkRows" :mode="mode" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="-3">
|
||||||
|
<OverviewKeypairs v-if="vm" v-model="vm" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="advanced"
|
||||||
|
:label="t('harvester.tab.advanced')"
|
||||||
|
:weight="-4"
|
||||||
|
>
|
||||||
|
<CloudConfig
|
||||||
|
ref="yamlEditor"
|
||||||
|
:user-script="userScript"
|
||||||
|
:mode="mode"
|
||||||
|
:network-script="networkScript"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<Checkbox v-model="installUSBTablet" :mode="mode" class="check" type="checkbox" :label="t('harvester.virtualMachine.enableUsb')" />
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,283 @@
|
|||||||
|
<script>
|
||||||
|
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||||
|
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
|
import { HCI } from '../../../types';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMDetailsBasics',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
VMConsoleBar,
|
||||||
|
HarvesterIpAddress,
|
||||||
|
LabelValue,
|
||||||
|
InputOrDisplay
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
vmi: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
creationTimestamp() {
|
||||||
|
const date = new Date(this.value?.metadata?.creationTimestamp);
|
||||||
|
|
||||||
|
if (!date.getMonth) {
|
||||||
|
return UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${ date.getMonth() + 1 }/${ date.getDate() }/${ date.getUTCFullYear() }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
node() {
|
||||||
|
const node = this.vmi?.status?.nodeName || UNDEFINED;
|
||||||
|
|
||||||
|
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : node;
|
||||||
|
},
|
||||||
|
|
||||||
|
hostname() {
|
||||||
|
const hostName = this.vmi?.spec?.hostname || this.vmi?.status?.guestOSInfo?.hostname || this.t('harvester.virtualMachine.detail.GuestAgentNotInstalled');
|
||||||
|
|
||||||
|
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : hostName;
|
||||||
|
},
|
||||||
|
|
||||||
|
imageName() {
|
||||||
|
const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || [];
|
||||||
|
|
||||||
|
const image = imageList.find( (I) => {
|
||||||
|
return this.value.rootImageId === I.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return image?.spec?.displayName || 'N/A';
|
||||||
|
},
|
||||||
|
|
||||||
|
disks() {
|
||||||
|
const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||||
|
|
||||||
|
return disks.filter((disk) => {
|
||||||
|
return !!disk.bootOrder;
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (a.bootOrder < b.bootOrder) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
cdroms() {
|
||||||
|
const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
|
||||||
|
|
||||||
|
return disks.filter((disk) => {
|
||||||
|
return !!disk.cdrom;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
flavor() {
|
||||||
|
const domain = this.value?.spec?.template?.spec?.domain;
|
||||||
|
|
||||||
|
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
kernelRelease() {
|
||||||
|
const kernelRelease = this.vmi?.status?.guestOSInfo?.kernelRelease || this.t('harvester.virtualMachine.detail.GuestAgentNotInstalled');
|
||||||
|
|
||||||
|
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : kernelRelease;
|
||||||
|
},
|
||||||
|
|
||||||
|
operatingSystem() {
|
||||||
|
const operatingSystem = this.vmi?.status?.guestOSInfo?.prettyName || this.t('harvester.virtualMachine.detail.GuestAgentNotInstalled');
|
||||||
|
|
||||||
|
return this.isDown ? this.t('harvester.virtualMachine.detail.details.down') : operatingSystem;
|
||||||
|
},
|
||||||
|
|
||||||
|
isDown() {
|
||||||
|
return this.isEmpty(this.vmi);
|
||||||
|
},
|
||||||
|
|
||||||
|
machineType() {
|
||||||
|
return this.value?.spec?.template?.spec?.domain?.machine?.type || undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getDeviceType(o) {
|
||||||
|
if (o.disk) {
|
||||||
|
return 'Disk';
|
||||||
|
} else {
|
||||||
|
return 'CD-ROM';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEmpty(o) {
|
||||||
|
return o !== undefined && Object.keys(o).length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VMConsoleBar :resource="value" class="consoleBut" />
|
||||||
|
<div class="overview-basics">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="value.nameDisplay">
|
||||||
|
<template #value>
|
||||||
|
<div class="smart-row">
|
||||||
|
<div class="console">
|
||||||
|
{{ value.nameDisplay }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname">
|
||||||
|
<template #value>
|
||||||
|
<div>
|
||||||
|
{{ hostname }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.node')" :value="node">
|
||||||
|
<template #value>
|
||||||
|
<div>
|
||||||
|
{{ node }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.ipAddress')">
|
||||||
|
<template #value>
|
||||||
|
<HarvesterIpAddress v-model="value.id" :row="value" />
|
||||||
|
</template>
|
||||||
|
</LabelValue>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.created')" :value="creationTimestamp" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-divider" />
|
||||||
|
|
||||||
|
<h2>{{ t('harvester.virtualMachine.detail.tabs.configurations') }}</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<InputOrDisplay :name="t('harvester.virtualMachine.detail.details.bootOrder')" :value="disks" :mode="mode">
|
||||||
|
<template #value>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(disk) in disks" :key="disk.bootOrder">
|
||||||
|
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<InputOrDisplay :name="t('harvester.virtualMachine.detail.details.CDROMs')" :value="cdroms" :mode="mode">
|
||||||
|
<template #value>
|
||||||
|
<div>
|
||||||
|
<ul v-if="cdroms.length > 0">
|
||||||
|
<li v-for="(rom) in cdroms" :key="rom.name">
|
||||||
|
{{ rom.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span v-else>
|
||||||
|
{{ t("harvester.virtualMachine.detail.notAvailable") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.operatingSystem')" :value="operatingSystem" />
|
||||||
|
</div>
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.flavor')" :value="flavor" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.kernelRelease')" :value="kernelRelease" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.consoleBut {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-basics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-row-gap: 15px;
|
||||||
|
|
||||||
|
.badge-state {
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.console {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__ssh-key {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
<script>
|
||||||
|
import { REASON } from '@shell/config/table-headers';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VirtualMachineEvents',
|
||||||
|
|
||||||
|
components: { SortableTable },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
events: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const reason = {
|
||||||
|
...REASON,
|
||||||
|
canBeVariable: true,
|
||||||
|
width: 180
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventHeaders = [
|
||||||
|
reason,
|
||||||
|
{
|
||||||
|
name: 'resource',
|
||||||
|
label: 'Resource',
|
||||||
|
labelKey: 'clusterIndexPage.sections.events.resource.label',
|
||||||
|
value: 'displayInvolvedObject',
|
||||||
|
sort: ['involvedObject.kind', 'involvedObject.name'],
|
||||||
|
canBeVariable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
name: 'date',
|
||||||
|
label: 'Date',
|
||||||
|
labelKey: 'clusterIndexPage.sections.events.date.label',
|
||||||
|
value: 'lastTimestamp',
|
||||||
|
sort: 'lastTimestamp:desc',
|
||||||
|
formatter: 'LiveDate',
|
||||||
|
formatterOpts: { addSuffix: true },
|
||||||
|
width: 125,
|
||||||
|
defaultSort: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return { eventHeaders };
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SortableTable
|
||||||
|
:rows="events"
|
||||||
|
:headers="eventHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:search="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:paging="true"
|
||||||
|
:rows-per-page="10"
|
||||||
|
default-sort-by="date"
|
||||||
|
>
|
||||||
|
<template #cell:resource="{row, value}">
|
||||||
|
<div class="text-info">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.message">
|
||||||
|
{{ row.displayMessage }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
<script>
|
||||||
|
import isString from 'lodash/isString';
|
||||||
|
import { HCI } from '../../../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import impl from '../../../mixins/harvester-vm/impl';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [impl],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const hash = await allHash({ allSSHs: this.$store.dispatch('harvester/findAll', { type: HCI.SSH }) });
|
||||||
|
|
||||||
|
this.allSSHs = hash.allSSHs;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allSSHs: [],
|
||||||
|
sshKeys: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleShow(idx) {
|
||||||
|
const ssh = this.sshKeys[idx];
|
||||||
|
|
||||||
|
this.$set(this.sshKeys, idx, {
|
||||||
|
...ssh,
|
||||||
|
showKey: !ssh.showKey
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getKeys() {
|
||||||
|
return this.mergeAllSSHs(this.value?.spec);
|
||||||
|
},
|
||||||
|
|
||||||
|
isShow(id = '') {
|
||||||
|
const ssh = this.sshKeys.find(O => O?.data?.id === id) || {};
|
||||||
|
|
||||||
|
return ssh.showKey || false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
allSSHs(neu) {
|
||||||
|
const out = this.getKeys().map((ssh) => {
|
||||||
|
return {
|
||||||
|
id: ssh.id,
|
||||||
|
publicKey: isString(ssh.data) ? ssh.data : ssh.data?.spec?.publicKey,
|
||||||
|
showKey: this.isShow(ssh.id)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$set(this, 'sshKeys', out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="overview-sshKeys">
|
||||||
|
<div v-for="(ssh, index) in sshKeys" :key="index" class="row overview-sshKeys__item">
|
||||||
|
<div class="col span-4">
|
||||||
|
{{ ssh.id }}
|
||||||
|
</div>
|
||||||
|
<div class="col span-7 offset-1">
|
||||||
|
<div v-if="ssh.showKey" class="key-display">
|
||||||
|
{{ ssh.publicKey }}
|
||||||
|
<button class="btn btn-sm role-link hide-bar" @click="toggleShow(index)">
|
||||||
|
<i class="icon icon-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button v-else class="btn btn-sm role-link" @click="toggleShow(index)">
|
||||||
|
*******<i class="icons icon-show"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.overview-sshKeys {
|
||||||
|
text-align: left;
|
||||||
|
max-height: 700px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-display {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 30px;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
.hide-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VirtualMachineMigration',
|
||||||
|
|
||||||
|
components: { LabelValue },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
vmiResource: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { localResource: this.vmiResource };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
migrationState() {
|
||||||
|
return this.localResource?.status?.migrationState;
|
||||||
|
},
|
||||||
|
sourceNode() {
|
||||||
|
return this.migrationState?.sourceNode || 'N/A';
|
||||||
|
},
|
||||||
|
targetNode() {
|
||||||
|
return this.migrationState?.targetNode || 'N/A';
|
||||||
|
},
|
||||||
|
started() {
|
||||||
|
return this.migrationState?.startTimestamp || 'N/A';
|
||||||
|
},
|
||||||
|
ended() {
|
||||||
|
return this.migrationState?.endTimestamp || 'N/A';
|
||||||
|
},
|
||||||
|
message() {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
vmiResource: {
|
||||||
|
handler(neu) {
|
||||||
|
this.localResource = neu;
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.sourceNode')" :value="sourceNode" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.targetNode')" :value="targetNode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.started')" :value="started" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue :name="t('harvester.virtualMachine.detail.details.ended')" :value="ended" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
271
pkg/harvester/detail/kubevirt.io.virtualmachine/index.vue
Normal file
271
pkg/harvester/detail/kubevirt.io.virtualmachine/index.vue
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { EVENT, SERVICE, POD } from '@shell/config/types';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import VM_MIXIN from '../../mixins/harvester-vm';
|
||||||
|
import DashboardMetrics from '@shell/components/DashboardMetrics';
|
||||||
|
import { allHash, setPromiseResult } from '@shell/utils/promise';
|
||||||
|
import { allDashboardsExist } from '@shell/utils/grafana';
|
||||||
|
|
||||||
|
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
|
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
|
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
|
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
||||||
|
import PodAffinity from '@shell/components/form/PodAffinity';
|
||||||
|
import AccessCredentials from '../../edit/kubevirt.io.virtualmachine/VirtualMachineAccessCredentials';
|
||||||
|
import Events from './VirtualMachineTabs/VirtualMachineEvents';
|
||||||
|
import Migration from './VirtualMachineTabs/VirtualMachineMigration';
|
||||||
|
import OverviewBasics from './VirtualMachineTabs/VirtualMachineBasics';
|
||||||
|
import OverviewKeypairs from './VirtualMachineTabs/VirtualMachineKeypairs';
|
||||||
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
import Labels from '@shell/components/form/Labels';
|
||||||
|
|
||||||
|
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMIDetailsPage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
Events,
|
||||||
|
OverviewBasics,
|
||||||
|
Volume,
|
||||||
|
Network,
|
||||||
|
OverviewKeypairs,
|
||||||
|
CloudConfig,
|
||||||
|
Migration,
|
||||||
|
DashboardMetrics,
|
||||||
|
AccessCredentials,
|
||||||
|
NodeScheduling,
|
||||||
|
PodAffinity,
|
||||||
|
KeyValue,
|
||||||
|
Labels,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, VM_MIXIN],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
switchToCloud: false,
|
||||||
|
VM_METRICS_DETAIL_URL,
|
||||||
|
showVmMetrics: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
pods: this.$store.dispatch(`${ inStore }/findAll`, { type: POD }),
|
||||||
|
services: this.$store.dispatch(`${ inStore }/findAll`, { type: SERVICE }),
|
||||||
|
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
||||||
|
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
||||||
|
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
||||||
|
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
|
||||||
|
setPromiseResult(
|
||||||
|
allDashboardsExist(this.$store, this.currentCluster.id, [VM_METRICS_DETAIL_URL], 'harvester'),
|
||||||
|
this,
|
||||||
|
'showVmMetrics',
|
||||||
|
'Determine vm metrics'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['currentCluster']),
|
||||||
|
|
||||||
|
vmi() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const vmiList = this.$store.getters[`${ inStore }/all`](HCI.VMI) || [];
|
||||||
|
const vmi = vmiList.find( (VMI) => {
|
||||||
|
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.value?.metadata?.uid;
|
||||||
|
});
|
||||||
|
|
||||||
|
return vmi;
|
||||||
|
},
|
||||||
|
|
||||||
|
allEvents() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](EVENT);
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return this.allEvents.filter((e) => {
|
||||||
|
const { name, creationTimestamp } = this.value?.metadata || {};
|
||||||
|
const podName = this.value.podResource?.metadata?.name;
|
||||||
|
const pvcName = this.value.persistentVolumeClaimName || [];
|
||||||
|
|
||||||
|
const involvedName = e?.involvedObject?.name;
|
||||||
|
|
||||||
|
const matchPVC = pvcName.find(name => name === involvedName);
|
||||||
|
|
||||||
|
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
||||||
|
}).sort((a, b) => {
|
||||||
|
if (a.lastTimestamp > b.lastTimestamp) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
graphVars() {
|
||||||
|
return {
|
||||||
|
namespace: this.value.namespace,
|
||||||
|
vm: this.value.name
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onTabChanged({ tab }) {
|
||||||
|
if (tab.name === 'cloudConfig') {
|
||||||
|
this.$refs.yamlEditor?.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
const diskRows = this.getDiskRows(neu);
|
||||||
|
|
||||||
|
this.$set(this, 'diskRows', diskRows);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true" @changed="onTabChanged">
|
||||||
|
<Tab name="basics" :label="t('harvester.virtualMachine.detail.tabs.basics')" class="bordered-table" :weight="7">
|
||||||
|
<OverviewBasics v-model="value" :vmi="vmi" mode="view" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="disks" :label="t('harvester.tab.volume')" class="bordered-table" :weight="6">
|
||||||
|
<Volume
|
||||||
|
v-model="diskRows"
|
||||||
|
mode="view"
|
||||||
|
:namespace="value.metadata.namespace"
|
||||||
|
:vm="value"
|
||||||
|
:resource-type="value.type"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="networks" :label="t('harvester.virtualMachine.detail.tabs.networks')" class="bordered-table" :weight="5">
|
||||||
|
<Network v-model="networkRows" mode="view" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="keypairs" :label="t('harvester.virtualMachine.detail.tabs.keypairs')" class="bordered-table" :weight="3">
|
||||||
|
<OverviewKeypairs v-model="value" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="showVmMetrics"
|
||||||
|
name="vm-metrics"
|
||||||
|
:label="t('harvester.virtualMachine.detail.tabs.metrics')"
|
||||||
|
:weight="2.5"
|
||||||
|
>
|
||||||
|
<template #default="props">
|
||||||
|
<DashboardMetrics
|
||||||
|
v-if="props.active"
|
||||||
|
:detail-url="VM_METRICS_DETAIL_URL"
|
||||||
|
graph-height="550px"
|
||||||
|
:has-summary-and-detail="false"
|
||||||
|
:vars="graphVars"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="nodeScheduling" :label="t('workload.container.titles.nodeScheduling')" :weight="2.4">
|
||||||
|
<template #default="{active}">
|
||||||
|
<NodeScheduling v-if="spec" :key="active" :mode="mode" :value="spec.template.spec" :nodes="nodesIdOptions" />
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab :label="t('harvester.tab.vmScheduling')" name="vmScheduling" :weight="2.3">
|
||||||
|
<template #default="{active}">
|
||||||
|
<PodAffinity
|
||||||
|
v-if="spec"
|
||||||
|
:key="active"
|
||||||
|
:mode="mode"
|
||||||
|
:value="spec.template.spec"
|
||||||
|
:nodes="nodes"
|
||||||
|
:all-namespaces-option-available="true"
|
||||||
|
:namespaces="filteredNamespaces"
|
||||||
|
:overwrite-labels="affinityLabels"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab :label="t('harvester.tab.accessCredentials')" class="bordered-table" name="accessCredentials" :weight="2.2">
|
||||||
|
<AccessCredentials mode="view" :value="accessCredentials" :resource="value" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="cloudConfig" :label="t('harvester.virtualMachine.detail.tabs.cloudConfig')" class="bordered-table" :weight="2">
|
||||||
|
<CloudConfig
|
||||||
|
ref="yamlEditor"
|
||||||
|
mode="view"
|
||||||
|
:user-script="userScript"
|
||||||
|
:network-script="networkScript"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="event" :label="t('harvester.virtualMachine.detail.tabs.events')" :weight="1">
|
||||||
|
<Events :resource="vmi" :events="events" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="migration" :label="t('harvester.virtualMachine.detail.tabs.migration')">
|
||||||
|
<Migration v-model="value" :vmi-resource="vmi" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="instanceLabel"
|
||||||
|
:label="t('harvester.tab.instanceLabel')"
|
||||||
|
:weight="-99"
|
||||||
|
>
|
||||||
|
<Labels
|
||||||
|
:default-container-class="'labels-and-annotations-container'"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:display-side-by-side="false"
|
||||||
|
:show-annotations="false"
|
||||||
|
:show-label-title="false"
|
||||||
|
>
|
||||||
|
<template #labels="{toggler}">
|
||||||
|
<KeyValue
|
||||||
|
key="labels"
|
||||||
|
:value="value.instanceLabels"
|
||||||
|
:protected-keys="value.systemLabels || []"
|
||||||
|
:toggle-filter="toggler"
|
||||||
|
:add-label="t('labels.addLabel')"
|
||||||
|
:mode="mode"
|
||||||
|
:read-allowed="false"
|
||||||
|
:value-can-be-empty="true"
|
||||||
|
@input="value.setInstanceLabels($event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Labels>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
106
pkg/harvester/detail/loadbalancer.harvesterhci.io.ippool.vue
Normal file
106
pkg/harvester/detail/loadbalancer.harvesterhci.io.ippool.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { NETWORK_HEADERS } from '@pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
SortableTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = { ipPools: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }) };
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
networks() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const networks = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT);
|
||||||
|
|
||||||
|
return networks.filter(n => n?.id === this.value?.spec?.selector?.network);
|
||||||
|
},
|
||||||
|
|
||||||
|
networkHeaders() {
|
||||||
|
return NETWORK_HEADERS;
|
||||||
|
},
|
||||||
|
|
||||||
|
ranges() {
|
||||||
|
return this.value.spec.ranges;
|
||||||
|
},
|
||||||
|
|
||||||
|
rangeHeaders() {
|
||||||
|
return [{
|
||||||
|
name: 'subnet',
|
||||||
|
label: this.t('harvester.ipPool.subnet.label'),
|
||||||
|
value: 'subnet',
|
||||||
|
}, {
|
||||||
|
name: 'gateway',
|
||||||
|
label: this.t('harvester.ipPool.gateway.label'),
|
||||||
|
value: 'gateway',
|
||||||
|
}, {
|
||||||
|
name: 'startIP',
|
||||||
|
label: this.t('harvester.ipPool.startIP.label'),
|
||||||
|
value: 'rangeStart',
|
||||||
|
}, {
|
||||||
|
name: 'endIP',
|
||||||
|
label: this.t('harvester.ipPool.endIP.label'),
|
||||||
|
value: 'rangeEnd',
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ResourceTabs
|
||||||
|
v-model="value"
|
||||||
|
:need-related="false"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="network"
|
||||||
|
label-key="harvester.ipPool.network.label"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
key-field="_key"
|
||||||
|
:headers="networkHeaders"
|
||||||
|
:rows="networks"
|
||||||
|
:row-actions="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="range"
|
||||||
|
label-key="harvester.ipPool.tabs.range"
|
||||||
|
:weight="89"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
key-field="_key"
|
||||||
|
:headers="rangeHeaders"
|
||||||
|
:rows="ranges"
|
||||||
|
:row-actions="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,190 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import { HCI } from '@pkg/harvester/types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { KEY, VALUE } from '@shell/config/table-headers';
|
||||||
|
import { VM_HEADERS } from '@pkg/harvester/list/kubevirt.io.virtualmachine';
|
||||||
|
import { matching } from '@shell/utils/selector';
|
||||||
|
import { IP_POOL_HEADERS } from '../product';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
SortableTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
ipPools: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IP_POOL }),
|
||||||
|
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
ipPools() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const ipPools = this.$store.getters[`${ inStore }/all`](HCI.IP_POOL);
|
||||||
|
|
||||||
|
return ipPools.filter(i => i.id === this.value.status.allocatedAddress.ipPool);
|
||||||
|
},
|
||||||
|
|
||||||
|
ipPoolHeaders() {
|
||||||
|
return IP_POOL_HEADERS;
|
||||||
|
},
|
||||||
|
|
||||||
|
listeners() {
|
||||||
|
const listeners = this.value?.spec?.listeners;
|
||||||
|
|
||||||
|
return listeners;
|
||||||
|
},
|
||||||
|
|
||||||
|
listenerHeaders() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: this.t('tableHeaders.name'),
|
||||||
|
value: 'name',
|
||||||
|
sort: 'name:desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'port',
|
||||||
|
label: this.t('tableHeaders.port'),
|
||||||
|
value: 'port',
|
||||||
|
sort: 'port:desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'protocol',
|
||||||
|
label: this.t('tableHeaders.protocol'),
|
||||||
|
value: 'protocol',
|
||||||
|
sort: 'protocol:desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'backendPort',
|
||||||
|
label: this.t('harvester.loadBalancer.listeners.backendPort.label'),
|
||||||
|
value: 'backendPort',
|
||||||
|
sort: 'backendPort:desc',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
backendServerSelectors() {
|
||||||
|
return Object.keys(this.value.spec?.backendServerSelector || {}).map(key => ({
|
||||||
|
key,
|
||||||
|
value: this.value.spec.backendServerSelector[key],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceSelectorInfoHeaders() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...KEY,
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
VALUE,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
vmHeaders() {
|
||||||
|
const filterNames = ['state', 'ip', 'node'];
|
||||||
|
|
||||||
|
return VM_HEADERS.filter(h => !filterNames.includes(h.name));
|
||||||
|
},
|
||||||
|
|
||||||
|
vms() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vms = this.$store.getters[`${ inStore }/all`](HCI.VM).filter(vm => vm.metadata.namespace === this.value.metadata.namespace);
|
||||||
|
const match = matching(vms, this.value?.spec?.backendServerSelector, 'spec.template.metadata.labels');
|
||||||
|
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ResourceTabs
|
||||||
|
v-model="value"
|
||||||
|
:need-related="false"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
v-if="value.spec.ipam === 'pool'"
|
||||||
|
name="ipPool"
|
||||||
|
label-key="harvester.loadBalancer.ipPool.label"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
key-field="_key"
|
||||||
|
:headers="ipPoolHeaders"
|
||||||
|
:rows="ipPools"
|
||||||
|
:row-actions="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="value.spec.workloadType === 'vm'"
|
||||||
|
name="vm"
|
||||||
|
:label="t('harvester.loadBalancer.backendServers.label')"
|
||||||
|
class="bordered-table"
|
||||||
|
:weight="98"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:rows="vms"
|
||||||
|
:headers="vmHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:row-actions="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:search="vms.length > 10 ? true : false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="value.spec.workloadType === 'vm'"
|
||||||
|
name="listeners"
|
||||||
|
:label="t('harvester.loadBalancer.tabs.listeners')"
|
||||||
|
class="bordered-table"
|
||||||
|
:weight="89"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
key-field="_key"
|
||||||
|
:headers="listenerHeaders"
|
||||||
|
:rows="listeners"
|
||||||
|
:row-actions="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="value.spec.workloadType === 'vm'"
|
||||||
|
name="selector"
|
||||||
|
:label="t('harvester.loadBalancer.tabs.backendServer')"
|
||||||
|
class="bordered-table"
|
||||||
|
:weight="79"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
key-field="_key"
|
||||||
|
:headers="serviceSelectorInfoHeaders"
|
||||||
|
:rows="backendServerSelectors"
|
||||||
|
:row-actions="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:show-headers="true"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</template>
|
||||||
90
pkg/harvester/detail/network.harvesterhci.io.vlanconfig.vue
Normal file
90
pkg/harvester/detail/network.harvesterhci.io.vlanconfig.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { STATE, NAME, AGE } from '@shell/config/table-headers';
|
||||||
|
import { matching } from '@shell/utils/selector';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { isEmpty } from '@shell/utils/object';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
SortableTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.$store.dispatch('harvester/findAll', { type: NODE });
|
||||||
|
|
||||||
|
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
nodeHeaders() {
|
||||||
|
return [
|
||||||
|
STATE,
|
||||||
|
NAME,
|
||||||
|
{
|
||||||
|
name: 'host-ip',
|
||||||
|
labelKey: 'tableHeaders.hostIp',
|
||||||
|
search: ['internalIp'],
|
||||||
|
value: 'internalIp',
|
||||||
|
sort: ['internalIp'],
|
||||||
|
},
|
||||||
|
AGE,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
nodes() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
||||||
|
const selector = this.value?.spec?.nodeSelector;
|
||||||
|
|
||||||
|
if (!isEmpty(selector)) {
|
||||||
|
return matching(nodes, selector);
|
||||||
|
} else {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ResourceTabs
|
||||||
|
v-model="value"
|
||||||
|
:need-related="false"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="node"
|
||||||
|
label-key="harvester.vlanConfig.titles.host"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
key-field="_key"
|
||||||
|
:headers="nodeHeaders"
|
||||||
|
:rows="nodes"
|
||||||
|
:row-actions="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</template>
|
||||||
134
pkg/harvester/dialog/CloneVmDialog.vue
Normal file
134
pkg/harvester/dialog/CloneVmDialog.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CloneVMModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
cloneData: true,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.name = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(buttonCb) {
|
||||||
|
// shadow clone
|
||||||
|
if (!this.cloneData) {
|
||||||
|
this.resources[0].goToClone();
|
||||||
|
buttonCb(false);
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deep clone
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('harvester.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.cloneVM.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<Checkbox v-model="cloneData" class="mb-10" label-key="harvester.modal.cloneVM.type" />
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-show="cloneData"
|
||||||
|
v-model="name"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.modal.cloneVM.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||||
|
:disabled="cloneData && !name"
|
||||||
|
@click="create"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
168
pkg/harvester/dialog/ConfirmRelatedToRemoveDialog.vue
Normal file
168
pkg/harvester/dialog/ConfirmRelatedToRemoveDialog.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { alternateLabel } from '@shell/utils/platform';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
import CopyToClipboardText from '@shell/components/CopyToClipboardText';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name ConfirmRelatedToRemoveDialog
|
||||||
|
* @description Dialog component to confirm the related resources before removing the resource.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'ConfirmRelatedToRemoveDialog',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
Card,
|
||||||
|
CopyToClipboardText
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* @property resources to be deleted.
|
||||||
|
* @type {Resource[]} Array of the resource model's instance
|
||||||
|
*/
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { errors: [], confirmName: '' };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
|
warningMessageKey() {
|
||||||
|
return this.modalData.warningMessageKey;
|
||||||
|
},
|
||||||
|
|
||||||
|
names() {
|
||||||
|
return this.resources.map(obj => obj.nameDisplay).slice(0, 5);
|
||||||
|
},
|
||||||
|
|
||||||
|
resourceNames() {
|
||||||
|
return this.names.reduce((res, name, i) => {
|
||||||
|
if (i >= 5) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
res += `<b>${ escapeHtml(name) }</b>`;
|
||||||
|
if (i === this.names.length - 1) {
|
||||||
|
res += this.plusMore;
|
||||||
|
} else {
|
||||||
|
res += i === this.resources.length - 2 ? ' and ' : ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, '');
|
||||||
|
},
|
||||||
|
|
||||||
|
nameToMatch() {
|
||||||
|
return this.resources[0].nameDisplay;
|
||||||
|
},
|
||||||
|
|
||||||
|
plusMore() {
|
||||||
|
const remaining = this.resources.length - this.names.length;
|
||||||
|
|
||||||
|
return this.t('promptRemove.andOthers', { count: remaining });
|
||||||
|
},
|
||||||
|
|
||||||
|
type() {
|
||||||
|
const types = new Set(this.resources.reduce((array, each) => {
|
||||||
|
array.push(each.type);
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}, []));
|
||||||
|
|
||||||
|
if (types.size > 1) {
|
||||||
|
return this.t('generic.resource', { count: this.resources.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = this.resources[0]?.schema;
|
||||||
|
|
||||||
|
if ( !schema ) {
|
||||||
|
return `resource${ this.resources.length === 1 ? '' : 's' }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$store.getters['type-map/labelFor'](schema, this.resources.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDisabled() {
|
||||||
|
return this.confirmName !== this.nameToMatch;
|
||||||
|
},
|
||||||
|
|
||||||
|
protip() {
|
||||||
|
return this.t('promptRemove.protip', { alternateLabel });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
escapeHtml,
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.confirmName = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove(buttonDone) {
|
||||||
|
try {
|
||||||
|
for (const resource of this.resources) {
|
||||||
|
await resource.remove();
|
||||||
|
}
|
||||||
|
buttonDone(true);
|
||||||
|
this.close();
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = exceptionToErrorsArray(e);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="prompt-related" :show-highlight-border="false">
|
||||||
|
<h4 slot="title" class="text-default-text">
|
||||||
|
{{ t('promptRemove.title') }}
|
||||||
|
</h4>
|
||||||
|
<div slot="body" class="pl-10 pr-10">
|
||||||
|
<span
|
||||||
|
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<div class="mt-10 mb-10">
|
||||||
|
<span
|
||||||
|
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-10">
|
||||||
|
<CopyToClipboardText :text="nameToMatch" />
|
||||||
|
</div>
|
||||||
|
<input id="confirm" v-model="confirmName" type="text" />
|
||||||
|
<div class="text-info mt-20">
|
||||||
|
{{ protip }}
|
||||||
|
</div>
|
||||||
|
<Banner v-for="(error, i) in errors" :key="i" class="" color="error" :label="error" />
|
||||||
|
</div>
|
||||||
|
<template #actions>
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton mode="delete" class="btn bg-error ml-10" :disabled="deleteDisabled" @click="remove" />
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
134
pkg/harvester/dialog/DeepCloneVmDialog.vue
Normal file
134
pkg/harvester/dialog/DeepCloneVmDialog.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CloneVMModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
cloneData: true,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.name = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(buttonCb) {
|
||||||
|
// shadow clone
|
||||||
|
if (!this.cloneData) {
|
||||||
|
this.resources[0].goToClone();
|
||||||
|
buttonCb(false);
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deep clone
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('harvester.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.cloneVM.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<Checkbox v-model="cloneData" class="mb-10" label-key="harvester.modal.cloneVM.type" />
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-show="cloneData"
|
||||||
|
v-model="name"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.modal.cloneVM.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||||
|
:disabled="cloneData && !name"
|
||||||
|
@click="create"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
pkg/harvester/dialog/EnablePassthrough.vue
Normal file
118
pkg/harvester/dialog/EnablePassthrough.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script>
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEnablePassthrough',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
// isSingleProduct == this is a standalone Harvester cluster
|
||||||
|
const isSingleProduct = this.$store.getters['isSingleProduct'];
|
||||||
|
let userName = 'admin';
|
||||||
|
|
||||||
|
// if this is imported Harvester, there may be users other than 'admin
|
||||||
|
if (!isSingleProduct) {
|
||||||
|
const user = this.$store.getters['auth/v3User'];
|
||||||
|
|
||||||
|
userName = user?.username || user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.resources.length; i++) {
|
||||||
|
const actionResource = this.resources[i];
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const pt = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.PCI_CLAIM,
|
||||||
|
metadata: {
|
||||||
|
name: actionResource.metadata.name,
|
||||||
|
ownerReferences: [{
|
||||||
|
apiVersion: 'devices.harvesterhci.io/v1beta1',
|
||||||
|
kind: 'PCIDevice',
|
||||||
|
name: actionResource.metadata.name,
|
||||||
|
uid: actionResource.metadata.uid,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
address: actionResource.status.address,
|
||||||
|
nodeName: actionResource.status.nodeName,
|
||||||
|
userName
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pt.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('harvester.pci.claimError', { name: escapeHtml(actionResource.metadata.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<h4
|
||||||
|
slot="title"
|
||||||
|
v-clean-html="t('promptRemove.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
{{ t('harvester.pci.enablePassthroughWarning') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton mode="enable" @click="save" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
107
pkg/harvester/dialog/EnableSriovDevice.vue
Normal file
107
pkg/harvester/dialog/EnableSriovDevice.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEnableSriovDevice',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const numVFs = this.resources[0].spec?.numVFs || 1;
|
||||||
|
|
||||||
|
return { numVFs, numVFsHistory: numVFs };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
numVFs(neu) {
|
||||||
|
if (!parseFloat(neu) || parseFloat(neu) < 1) {
|
||||||
|
this.numVFs = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
const actionResource = this.resources[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.resources[0].spec.numVFs = this.numVFs;
|
||||||
|
await actionResource.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
this.resources[0].spec.numVFs = this.numVFsHistory;
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(actionResource.metadata.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template v-slot:title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('promptRemove.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="numVFs"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
required
|
||||||
|
:label="t('harvester.sriov.numVFs')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:actions>
|
||||||
|
<div class="buttons actions">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton mode="enable" @click="save" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
114
pkg/harvester/dialog/EnableVGpuDevice.vue
Normal file
114
pkg/harvester/dialog/EnableVGpuDevice.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEnableVGpuDevice',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: this.resources[0].spec?.vGPUTypeName,
|
||||||
|
availableTypes: Object.keys(this.resources[0].status?.availableTypes || {}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
const actionResource = this.resources[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.resources[0].spec.vGPUTypeName = this.type;
|
||||||
|
this.resources[0].spec.enabled = true;
|
||||||
|
await actionResource.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(actionResource.metadata.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template v-slot:title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.vgpu.enable.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="body">
|
||||||
|
<div class="type-field">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="type"
|
||||||
|
required
|
||||||
|
:options="availableTypes"
|
||||||
|
:searchable="true"
|
||||||
|
:label="t('harvester.vgpu.enable.type')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:actions>
|
||||||
|
<div class="buttons actions">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton mode="edit" :disabled="!type" @click="save" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&-field {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
159
pkg/harvester/dialog/HarvesterAddHotplugModal.vue
Normal file
159
pkg/harvester/dialog/HarvesterAddHotplugModal.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { PVC } from '@shell/config/types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HotplugModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Card, LabeledInput, LabeledSelect, Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
diskName: '',
|
||||||
|
volumeName: '',
|
||||||
|
errors: [],
|
||||||
|
allPVCs: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
PVCs() {
|
||||||
|
return this.allPVCs.filter(P => this.actionResource.metadata.namespace === P.metadata.namespace) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeOption() {
|
||||||
|
return sortBy(
|
||||||
|
this.PVCs
|
||||||
|
.filter( (pvc) => {
|
||||||
|
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !pvc.attachVM;
|
||||||
|
})
|
||||||
|
.map((pvc) => {
|
||||||
|
return {
|
||||||
|
label: pvc.metadata.name,
|
||||||
|
value: pvc.metadata.name
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.diskName = '';
|
||||||
|
this.volumeName = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (this.actionResource) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('addVolume', { volumeSourceName: this.volumeName, diskName: this.diskName }, {}, false);
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card ref="modal" name="modal" :show-highlight-border="false">
|
||||||
|
<h4 slot="title" v-clean-html="t('harvester.modal.hotplug.title')" class="text-default-text" />
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="diskName"
|
||||||
|
:label="t('generic.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="volumeName"
|
||||||
|
:label="t('harvester.fields.volume')"
|
||||||
|
:options="volumeOption"
|
||||||
|
class="mt-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button type="button" class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="!diskName || !volumeName"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
pkg/harvester/dialog/HarvesterBackupModal.vue
Normal file
125
pkg/harvester/dialog/HarvesterBackupModal.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterBackupModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
LabeledInput,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
backUpName: '',
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.backUpName = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (this.actionResource) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction(
|
||||||
|
'backup',
|
||||||
|
{ name: this.backUpName },
|
||||||
|
{},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch(
|
||||||
|
'growl/success',
|
||||||
|
{
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.backup.success', { backUpName: this.backUpName })
|
||||||
|
},
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<h4
|
||||||
|
slot="title"
|
||||||
|
v-clean-html="t('harvester.modal.backup.addBackup')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput v-model="backUpName" :label="t('generic.name')" required />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton mode="create" :disabled="!backUpName" @click="save" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
pkg/harvester/dialog/HarvesterCloneTemplate.vue
Normal file
147
pkg/harvester/dialog/HarvesterCloneTemplate.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterCloneTemplateModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
LabeledInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
templateName: '',
|
||||||
|
description: '',
|
||||||
|
withData: false,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.templateName = '';
|
||||||
|
this.description = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction(
|
||||||
|
'createTemplate',
|
||||||
|
{
|
||||||
|
name: this.templateName, description: this.description, withData: this.withData
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch(
|
||||||
|
'growl/success',
|
||||||
|
{
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t(
|
||||||
|
'harvester.modal.createTemplate.message.success',
|
||||||
|
{ templateName: this.templateName }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.createTemplate.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<Checkbox v-model="withData" class="mb-10" label="With Data" />
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="templateName"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.modal.createTemplate.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="description"
|
||||||
|
:label="t('harvester.modal.createTemplate.description')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:disabled="!templateName"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
160
pkg/harvester/dialog/HarvesterEjectCDROMDialog.vue
Normal file
160
pkg/harvester/dialog/HarvesterEjectCDROMDialog.vue
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEjectCDROMModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const allDisk = [];
|
||||||
|
const disks = this.resources[0].spec.template.spec.domain.devices.disks;
|
||||||
|
|
||||||
|
if (Array.isArray(disks)) {
|
||||||
|
disks.forEach((D) => {
|
||||||
|
if (D.cdrom) {
|
||||||
|
allDisk.push({
|
||||||
|
name: D.name,
|
||||||
|
value: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allDisk,
|
||||||
|
errors: [],
|
||||||
|
diskNames: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
isDeleteDisabled() {
|
||||||
|
return this.diskNames.length === 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateNames(names) {
|
||||||
|
this.diskNames = names;
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove(buttonDone) {
|
||||||
|
try {
|
||||||
|
await this.actionResource.doAction('ejectCdRom', { diskNames: this.diskNames });
|
||||||
|
this.close();
|
||||||
|
buttonDone(true);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.backupName = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
allDisk: {
|
||||||
|
handler(neu) {
|
||||||
|
const diskNames = [];
|
||||||
|
|
||||||
|
neu.forEach((D) => {
|
||||||
|
if (D.value) {
|
||||||
|
diskNames.push(D.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$set(this, 'diskNames', diskNames);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.ejectCDROM.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<span class="text-info mb-10">
|
||||||
|
{{ t('harvester.modal.ejectCDROM.operationTip') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
v-for="disk in allDisk"
|
||||||
|
:key="disk.name"
|
||||||
|
v-model="disk.value"
|
||||||
|
:label="disk.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner color="warning">
|
||||||
|
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
|
||||||
|
</Banner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="delete"
|
||||||
|
:disabled="!diskNames.length"
|
||||||
|
class="btn bg-error ml-10"
|
||||||
|
@click="remove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
195
pkg/harvester/dialog/HarvesterExportImageDialog.vue
Normal file
195
pkg/harvester/dialog/HarvesterExportImageDialog.vue
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterExportImageDialog',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = { storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }) };
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
|
||||||
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find(s => s.isDefault);
|
||||||
|
|
||||||
|
this.$set(this, 'storageClassName', defaultStorage?.metadata?.name || 'longhorn');
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const namespace = this.$store.getters['defaultNamespace'] || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
namespace,
|
||||||
|
storageClassName: '',
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
namespaces() {
|
||||||
|
const choices = this.$store.getters['harvester/all'](NAMESPACE).filter( N => !N.isSystem);
|
||||||
|
|
||||||
|
const out = sortBy(
|
||||||
|
choices.map((obj) => {
|
||||||
|
return {
|
||||||
|
label: obj.nameDisplay,
|
||||||
|
value: obj.id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
disableSave() {
|
||||||
|
return !(this.name && this.namespace && this.storageClassName);
|
||||||
|
},
|
||||||
|
|
||||||
|
storageClassOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
|
const out = storages.filter(s => !s.parameters?.backingImage).map((s) => {
|
||||||
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: s.name,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.name = '';
|
||||||
|
this.namespace = '';
|
||||||
|
this.storageClassName = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('export', {
|
||||||
|
displayName: this.name,
|
||||||
|
namespace: this.namespace,
|
||||||
|
storageClassName: this.storageClassName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.exportImage.message.success', { name: this.name })
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.exportImage.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="namespace"
|
||||||
|
:label="t('harvester.modal.exportImage.namespace')"
|
||||||
|
:options="namespaces"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="name"
|
||||||
|
:label="t('harvester.modal.exportImage.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="storageClassName"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
:label="t('harvester.storage.storageClass.label')"
|
||||||
|
class="mt-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:disabled="disableSave"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
151
pkg/harvester/dialog/HarvesterMaintenanceDialog.vue
Normal file
151
pkg/harvester/dialog/HarvesterMaintenanceDialog.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { BadgeState } from '@components/BadgeState';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
BadgeState
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
errors: [],
|
||||||
|
unhealthyVM: '',
|
||||||
|
force: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async apply(buttonCb) {
|
||||||
|
this.errors = [];
|
||||||
|
this.unhealthyVM = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('maintenancePossible');
|
||||||
|
|
||||||
|
if (this.force) {
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
await this.actionResource.doAction('enableMaintenanceMode', { force: 'true' });
|
||||||
|
buttonCb(true);
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} else if (res._status === 200 || res._status === 204) {
|
||||||
|
const res = await this.actionResource.doAction('listUnhealthyVM');
|
||||||
|
|
||||||
|
if (res.message) {
|
||||||
|
this.unhealthyVM = res;
|
||||||
|
buttonCb(false);
|
||||||
|
} else {
|
||||||
|
await this.actionResource.doAction('enableMaintenanceMode', { force: 'false' });
|
||||||
|
buttonCb(true);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const error = [e?.data] || exceptionToErrorsArray(e);
|
||||||
|
|
||||||
|
this.errors = error;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.host.enableMaintenance.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
v-model="force"
|
||||||
|
label-key="harvester.host.enableMaintenance.force"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Banner color="warning" :label="t('harvester.host.enableMaintenance.protip')" class="mb-0" />
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
|
||||||
|
<div v-if="unhealthyVM">
|
||||||
|
<Banner color="error mb-5">
|
||||||
|
<p>
|
||||||
|
{{ unhealthyVM.message }}
|
||||||
|
</p>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="vm-list mb-5">
|
||||||
|
<BadgeState
|
||||||
|
v-for="vm in unhealthyVM.vms"
|
||||||
|
:key="vm"
|
||||||
|
color="bg-error mb-5 mr-5"
|
||||||
|
:label="vm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
@click="apply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
179
pkg/harvester/dialog/HarvesterMigrationDialog.vue
Normal file
179
pkg/harvester/dialog/HarvesterMigrationDialog.vue
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Card, LabeledSelect
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
try {
|
||||||
|
if (!this.actionResource.hasAction('findMigratableNodes')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await this.actionResource.$dispatch('resourceAction', {
|
||||||
|
resource: this.actionResource,
|
||||||
|
actionName: 'findMigratableNodes',
|
||||||
|
body: {},
|
||||||
|
opt: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.availableNodes = res.nodes;
|
||||||
|
} catch (err) {
|
||||||
|
this.actionResource.$dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error'),
|
||||||
|
err: err.data || err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
nodeName: '',
|
||||||
|
errors: [],
|
||||||
|
availableNodes: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
vmi() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI);
|
||||||
|
const resource = vmiResources.find(VMI => VMI.id === this.actionResource?.id) || null;
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeNameList() {
|
||||||
|
const nodes = this.$store.getters['harvester/all'](NODE);
|
||||||
|
|
||||||
|
return nodes.filter((n) => {
|
||||||
|
// do not allow to migrate to self node
|
||||||
|
return !!this.availableNodes.includes(n.id);
|
||||||
|
}).map((n) => {
|
||||||
|
let label = n?.metadata?.name;
|
||||||
|
const value = n?.metadata?.name;
|
||||||
|
const custom = n?.metadata?.annotations?.[HCI_ANNOTATIONS.HOST_CUSTOM_NAME];
|
||||||
|
|
||||||
|
if (custom) {
|
||||||
|
label = custom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.nodeName = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async apply(buttonDone) {
|
||||||
|
if (!this.actionResource) {
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.nodeName) {
|
||||||
|
const name = this.$store.getters['i18n/t']('harvester.modal.migration.fields.nodeName.label');
|
||||||
|
const message = this.$store.getters['i18n/t']('validation.required', { key: name });
|
||||||
|
|
||||||
|
this.$set(this, 'errors', [message]);
|
||||||
|
buttonDone(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false);
|
||||||
|
|
||||||
|
buttonDone(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.migration.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="nodeName"
|
||||||
|
:label="t('harvester.modal.migration.fields.nodeName.label')"
|
||||||
|
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||||
|
:options="nodeNameList"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="!nodeName"
|
||||||
|
@click="apply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
167
pkg/harvester/dialog/HarvesterRestoreDialog.vue
Normal file
167
pkg/harvester/dialog/HarvesterRestoreDialog.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterRestoreModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
Card,
|
||||||
|
LabeledSelect
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const hash = await allHash({ backups: this.$store.dispatch('harvester/findAll', { type: HCI.BACKUP }) });
|
||||||
|
|
||||||
|
this.backups = hash.backups;
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
backups: [],
|
||||||
|
backupName: '',
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
backupOption() {
|
||||||
|
const attachBackup = this.backups.filter((B) => {
|
||||||
|
return B.attachVM === this.actionResource?.metadata?.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return attachBackup.map((O) => {
|
||||||
|
return {
|
||||||
|
value: O.metadata.name,
|
||||||
|
label: O.metadata.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.backupName = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveRestore(buttonCb) {
|
||||||
|
const name = `restore-${ this.backupName }-${ randomStr(5).toLowerCase() }`;
|
||||||
|
|
||||||
|
if (!this.backupName) {
|
||||||
|
this.$set(this, 'errors', [
|
||||||
|
this.t('harvester.modal.restore.message.backup')
|
||||||
|
]);
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction(
|
||||||
|
'restore',
|
||||||
|
{ backupName: this.backupName, name },
|
||||||
|
{},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch(
|
||||||
|
'growl/success',
|
||||||
|
{
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.restore.success', { name: this.backupName })
|
||||||
|
},
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.restore.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="backupName"
|
||||||
|
:label="t('harvester.modal.restore.selectBackup')"
|
||||||
|
:localized-label="true"
|
||||||
|
:options="backupOption"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:disabled="!backupName"
|
||||||
|
@click="saveRestore"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
221
pkg/harvester/dialog/HarvesterSupportBundle.vue
Normal file
221
pkg/harvester/dialog/HarvesterSupportBundle.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<script>
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import GraphCircle from '@shell/components/graph/Circle';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AppModal from '@shell/components/AppModal.vue';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SupportBundle',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
GraphCircle,
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
AppModal,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
url: '',
|
||||||
|
description: '',
|
||||||
|
errors: [],
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
bundlePending() {
|
||||||
|
return this.$store.getters['harvester-common/isBundlePending'];
|
||||||
|
},
|
||||||
|
|
||||||
|
isShowBundleModal() {
|
||||||
|
return this.$store.getters['harvester-common/isShowBundleModal'];
|
||||||
|
},
|
||||||
|
|
||||||
|
percentage() {
|
||||||
|
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
isShowBundleModal: {
|
||||||
|
handler(show) {
|
||||||
|
if (show) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isOpen = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.url = '';
|
||||||
|
this.description = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
stringify,
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||||
|
this.backUpName = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
|
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
||||||
|
const namespace = 'harvester-system';
|
||||||
|
|
||||||
|
const bundleCrd = {
|
||||||
|
apiVersion: 'harvesterhci.io/v1beta1',
|
||||||
|
type: HCI.SUPPORT_BUNDLE,
|
||||||
|
kind: 'SupportBundle',
|
||||||
|
metadata: {
|
||||||
|
name,
|
||||||
|
namespace
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
issueURL: this.url,
|
||||||
|
description: this.description
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bundleValue.save();
|
||||||
|
|
||||||
|
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||||
|
this.$store.dispatch('harvester-common/bundleProgress', { root: true });
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bundleModal">
|
||||||
|
<app-modal
|
||||||
|
v-if="isOpen"
|
||||||
|
name="bundle-modal"
|
||||||
|
:click-to-close="false"
|
||||||
|
:width="550"
|
||||||
|
:height="390"
|
||||||
|
class="remove-modal support-modal"
|
||||||
|
>
|
||||||
|
<div class="p-20">
|
||||||
|
<h2>
|
||||||
|
{{ t('harvester.modal.bundle.title') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!bundlePending"
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="url"
|
||||||
|
:label="t('harvester.modal.bundle.url')"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="description"
|
||||||
|
:label="t('harvester.modal.bundle.description')"
|
||||||
|
type="multiline"
|
||||||
|
:min-height="120"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
|
<div class="circle">
|
||||||
|
<GraphCircle
|
||||||
|
primary-stroke-color="green"
|
||||||
|
secondary-stroke-color="white"
|
||||||
|
:stroke-width="6"
|
||||||
|
:percentage="percentage"
|
||||||
|
:show-text="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(err, idx) in errors"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<Banner
|
||||||
|
color="error"
|
||||||
|
:label="stringify(err)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer mt-20">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.close') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
type="submit"
|
||||||
|
mode="generate"
|
||||||
|
class="btn btn-sm bg-primary"
|
||||||
|
:disabled="bundlePending"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</app-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bundleModal {
|
||||||
|
.support-modal {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-spinner {
|
||||||
|
font-size: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 218px;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
padding-top: 20px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
pkg/harvester/dialog/HarvesterUnplugVolume.vue
Normal file
118
pkg/harvester/dialog/HarvesterUnplugVolume.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHotUnplugModal',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { errors: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState('action-menu', ['modalData']),
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
diskName() {
|
||||||
|
return this.modalData.diskName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch(
|
||||||
|
'growl/success',
|
||||||
|
{
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
||||||
|
},
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card ref="modal" name="modal" :show-highlight-border="false">
|
||||||
|
<h4
|
||||||
|
slot="title"
|
||||||
|
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button type="button" class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
|
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
|
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
149
pkg/harvester/dialog/HarvesterVMSnapshotDialog.vue
Normal file
149
pkg/harvester/dialog/HarvesterVMSnapshotDialog.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { BACKUP_TYPE } from '../config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVMSnapshotDialog',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
LabeledInput,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
snapshotName: '',
|
||||||
|
snapshotNamespace: '',
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.snapshotNamespace = '';
|
||||||
|
this.snapshotName = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (this.actionResource) {
|
||||||
|
try {
|
||||||
|
const snapshot = await this.$store.dispatch('harvester/create', {
|
||||||
|
metadata: {
|
||||||
|
name: this.snapshotName,
|
||||||
|
namespace: this.actionResource.metadata.namespace,
|
||||||
|
ownerReferences: this.getOwnerReferencesFromVM(this.actionResource)
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
source: {
|
||||||
|
apiGroup: 'kubevirt.io',
|
||||||
|
kind: 'VirtualMachine',
|
||||||
|
name: this.actionResource.metadata.name
|
||||||
|
},
|
||||||
|
type: BACKUP_TYPE.SNAPSHOT
|
||||||
|
},
|
||||||
|
type: HCI.BACKUP
|
||||||
|
});
|
||||||
|
|
||||||
|
await snapshot.save();
|
||||||
|
|
||||||
|
this.$store.dispatch(
|
||||||
|
'growl/success',
|
||||||
|
{
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.vmSnapshot.success', { name: this.snapshotName })
|
||||||
|
},
|
||||||
|
{ root: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
buttonCb(true);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getOwnerReferencesFromVM(resource) {
|
||||||
|
const name = resource.metadata.name;
|
||||||
|
const kind = resource.kind;
|
||||||
|
const apiVersion = 'kubevirt.io/v1';
|
||||||
|
const uid = resource?.metadata?.uid;
|
||||||
|
|
||||||
|
return [{
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
uid,
|
||||||
|
apiVersion,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<h4
|
||||||
|
slot="title"
|
||||||
|
v-clean-html="t('harvester.modal.vmSnapshot.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput v-model="actionResource.metadata.namespace" :disabled="true" :label="t('generic.namespace')" />
|
||||||
|
<LabeledInput v-model="snapshotName" class="mt-20" :label="t('generic.name')" required />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton mode="create" :disabled="!snapshotName" @click="save" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
138
pkg/harvester/dialog/HarvesterVlanConfigMigrateDialog.vue
Normal file
138
pkg/harvester/dialog/HarvesterVlanConfigMigrateDialog.vue
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { HCI } from '@pkg/harvester/types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
Card,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clusterNetwork: '',
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworks() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK);
|
||||||
|
|
||||||
|
return clusterNetworks.filter((c) => {
|
||||||
|
return c.id !== this.actionResource.spec?.clusterNetwork && c.id !== 'mgmt';
|
||||||
|
}).map((c) => {
|
||||||
|
const label = c.id;
|
||||||
|
const value = c.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.nodeName = '';
|
||||||
|
this.errors = [];
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async apply(buttonDone) {
|
||||||
|
try {
|
||||||
|
const data = clone(this.actionResource);
|
||||||
|
|
||||||
|
data.spec.clusterNetwork = this.clusterNetwork;
|
||||||
|
|
||||||
|
await this.$store.dispatch('harvester/request', {
|
||||||
|
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||||
|
method: 'PUT',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonDone(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.migration.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="clusterNetwork"
|
||||||
|
:label="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.label')"
|
||||||
|
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
|
||||||
|
:options="clusterNetworks"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
:disabled="!clusterNetwork"
|
||||||
|
@click="apply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
pkg/harvester/dialog/MessageBox.vue
Normal file
68
pkg/harvester/dialog/MessageBox.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHotUnplugModal',
|
||||||
|
|
||||||
|
components: { Card },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState('action-menu', ['modalData']),
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
ok() {
|
||||||
|
this.modalData?.callback('ok');
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card ref="modal" name="modal" :show-highlight-border="false">
|
||||||
|
<h4
|
||||||
|
slot="title"
|
||||||
|
class="text-default-text"
|
||||||
|
>
|
||||||
|
{{ t('generic.tip') }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<p v-clean-html="t(modalData.contentKey)"></p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button type="button" class="btn role-secondary mr-10" @click="ok">
|
||||||
|
{{ t('generic.ok') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
120
pkg/harvester/dialog/RestartVMDialog.vue
Normal file
120
pkg/harvester/dialog/RestartVMDialog.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Card,
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
vm: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { errors: [], resolve: null };
|
||||||
|
},
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.resolve();
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
apply(buttonDone) {
|
||||||
|
try {
|
||||||
|
this.vm.doActionGrowl('restart', {});
|
||||||
|
buttonDone(true);
|
||||||
|
this.resolve();
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err); // eslint-disable-line
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<app-modal
|
||||||
|
class="restart-modal"
|
||||||
|
name="restartDialog"
|
||||||
|
:width="600"
|
||||||
|
height="auto"
|
||||||
|
:click-to-close="false"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
class="prompt-restart"
|
||||||
|
:show-highlight-border="false"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
slot="title"
|
||||||
|
v-clean-html="t('harvester.modal.restart.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template slot="body">
|
||||||
|
<slot name="body">
|
||||||
|
<div
|
||||||
|
v-clean-html="t('harvester.modal.restart.tip')"
|
||||||
|
class="pl-10 pr-10"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
slot="actions"
|
||||||
|
class="bottom"
|
||||||
|
>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('harvester.modal.restart.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="restart"
|
||||||
|
@click="apply"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</app-modal>
|
||||||
|
</template>
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.restart-modal {
|
||||||
|
z-index: 45;
|
||||||
|
}
|
||||||
|
.prompt-restart {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
.banner {
|
||||||
|
margin-top: 0
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
169
pkg/harvester/dialog/RestoreSnapshotDialog.vue
Normal file
169
pkg/harvester/dialog/RestoreSnapshotDialog.vue
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { STORAGE_CLASS } from '@shell/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterRestoreSnapshotDialog',
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
|
Card,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
errors: [],
|
||||||
|
storageClassName: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = { storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }) };
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
|
||||||
|
if (this.showStorageClass) {
|
||||||
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find(s => s.isDefault);
|
||||||
|
|
||||||
|
const currentStorageName = this.resources[0].metadata?.annotations[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||||
|
|
||||||
|
this.$set(this, 'storageClassName', currentStorageName || defaultStorage?.metadata?.name || 'longhorn');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0] || {};
|
||||||
|
},
|
||||||
|
disableSave() {
|
||||||
|
return !(this.name && (this.showStorageClass ? this.storageClassName : true));
|
||||||
|
},
|
||||||
|
|
||||||
|
storageClassOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
|
const out = storages.filter(s => !s.parameters?.backingImage).filter((s) => {
|
||||||
|
return s.provisioner === this.actionResource.metadata?.annotations[HCI_ANNOTATIONS.STORAGE_PROVISIONER];
|
||||||
|
}).map((s) => {
|
||||||
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: s.name,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
showStorageClass() {
|
||||||
|
return this.actionResource?.volume?.source === 'data';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.name = '';
|
||||||
|
this.storageClassName = '';
|
||||||
|
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
async save(buttonCb) {
|
||||||
|
try {
|
||||||
|
const payload = { name: this.name };
|
||||||
|
|
||||||
|
if (this.showStorageClass) {
|
||||||
|
payload.storageClassName = this.storageClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await this.actionResource.doAction('restore', payload);
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('harvester.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.restoreSnapshot.success', { name: this.name })
|
||||||
|
}, { root: true });
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.restoreSnapshot.title') }}
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="name"
|
||||||
|
:label="t('harvester.modal.restoreSnapshot.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showStorageClass"
|
||||||
|
v-model="storageClassName"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
:label="t('harvester.storage.storageClass.label')"
|
||||||
|
class="mt-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:disabled="disableSave"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
103
pkg/harvester/dialog/SnapshotDialog.vue
Normal file
103
pkg/harvester/dialog/SnapshotDialog.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterSnapshotDialog',
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Card, LabeledInput
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
disableSave() {
|
||||||
|
return !this.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.name = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
async save(buttonCb) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('snapshot', { name: this.name });
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('harvester.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.snapshot.message.success', { name: this.name })
|
||||||
|
}, { root: true });
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.snapshot.title') }}
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="name"
|
||||||
|
:label="t('harvester.modal.snapshot.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:disabled="disableSave"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
pkg/harvester/dialog/VolumeCloneDialog.vue
Normal file
118
pkg/harvester/dialog/VolumeCloneDialog.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterPvcCloneDialog',
|
||||||
|
components: {
|
||||||
|
AsyncButton, Banner, Card, LabeledInput, Checkbox
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
cloneData: true,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0];
|
||||||
|
},
|
||||||
|
disableSave() {
|
||||||
|
return this.cloneData && !this.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.name = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (!this.cloneData) {
|
||||||
|
this.resources[0].goToClone();
|
||||||
|
buttonCb(false);
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('clone', { name: this.name });
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('harvester.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.volumeClone.message.success', { name: this.name })
|
||||||
|
}, { root: true });
|
||||||
|
this.close();
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', error);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = err?.data || err;
|
||||||
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
|
this.$set(this, 'errors', message);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.modal.volumeClone.title') }}
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<Checkbox v-model="cloneData" class="mb-10" label-key="harvester.modal.cloneVM.type" />
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-show="cloneData"
|
||||||
|
v-model="name"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.modal.volumeClone.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div slot="actions" class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn role-secondary mr-10" @click="close">
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
mode="create"
|
||||||
|
:disabled="disableSave"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
pkg/harvester/edit/harvesterhci.io.addon/generic.vue
Normal file
58
pkg/harvester/edit/harvesterhci.io.addon/generic.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditGenericAddon',
|
||||||
|
components: {
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
RadioGroup,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditSeederAddon',
|
||||||
|
components: {
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
RadioGroup,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
47
pkg/harvester/edit/harvesterhci.io.addon/index.vue
Normal file
47
pkg/harvester/edit/harvesterhci.io.addon/index.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditAddon',
|
||||||
|
|
||||||
|
components: { CruResource },
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
currentComponent() {
|
||||||
|
const name = this.value.metadata.name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return require(`./${ name }.vue`).default;
|
||||||
|
} catch {
|
||||||
|
return require(`./generic.vue`).default;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
@finish="save"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="currentComponent"
|
||||||
|
:value="value"
|
||||||
|
:register-before-hook="registerBeforeHook"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
<script>
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
const DEFAULT_VALUE = { image: { repo: 'rancher/harvester-nvidia-driver-toolkit' } };
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditAddonNvidiaDriverToolkit',
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
LabeledInput,
|
||||||
|
RadioGroup,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
initSpec: clone(this.value.spec),
|
||||||
|
valuesContentJson: this.parseValuesContent()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
valuesContentJson: {
|
||||||
|
handler(neu) {
|
||||||
|
this.update(neu);
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
parsingSpecError() {
|
||||||
|
return this.valuesContentJson && (this.valuesContentJson.image === undefined || this.valuesContentJson.driverLocation === undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseValuesContent() {
|
||||||
|
try {
|
||||||
|
return merge({}, DEFAULT_VALUE, jsyaml.load(this.value.spec.valuesContent));
|
||||||
|
} catch (err) {
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.$store.getters['i18n/t']('generic.notification.title.error'),
|
||||||
|
err: err.data || err,
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
return DEFAULT_VALUE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleEnable(v) {
|
||||||
|
this.resetSpec();
|
||||||
|
this.value.spec.enabled = v;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetSpec() {
|
||||||
|
this.value.spec = clone(this.initSpec);
|
||||||
|
|
||||||
|
this.valuesContentJson = this.parseValuesContent();
|
||||||
|
},
|
||||||
|
|
||||||
|
update(values) {
|
||||||
|
this.value.spec.valuesContent = jsyaml.dump(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="parsingSpecError"
|
||||||
|
color="error"
|
||||||
|
:label="t('harvester.addons.nvidiaDriverToolkit.parsingSpecError', null, { raw: true })"
|
||||||
|
/>
|
||||||
|
<Tabbed v-else :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.nvidiaDriverToolkit.titles.basic')"
|
||||||
|
:weight="1"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
:value="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@input="toggleEnable"
|
||||||
|
/>
|
||||||
|
<div v-if="value.spec.enabled">
|
||||||
|
<div v-if="valuesContentJson.image" class="row mb-15">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.image.repo"
|
||||||
|
:mode="mode"
|
||||||
|
:required="true"
|
||||||
|
label-key="harvester.addons.nvidiaDriverToolkit.image.repository"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.image.tag"
|
||||||
|
:mode="mode"
|
||||||
|
:required="true"
|
||||||
|
class="col span-6"
|
||||||
|
label-key="harvester.addons.nvidiaDriverToolkit.image.tag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-15">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.driverLocation"
|
||||||
|
:mode="mode"
|
||||||
|
:required="true"
|
||||||
|
label-key="harvester.addons.nvidiaDriverToolkit.driver.location"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditAddonPCI',
|
||||||
|
components: {
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
RadioGroup,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
191
pkg/harvester/edit/harvesterhci.io.addon/rancher-logging.vue
Normal file
191
pkg/harvester/edit/harvesterhci.io.addon/rancher-logging.vue
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<script>
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
|
||||||
|
const DEFAUL_VALUE = {
|
||||||
|
fluentbit: {
|
||||||
|
resources: {
|
||||||
|
limits: {
|
||||||
|
cpu: '200m',
|
||||||
|
memory: '200Mi'
|
||||||
|
},
|
||||||
|
requests: {
|
||||||
|
cpu: '50m',
|
||||||
|
memory: '50Mi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fluentd: {
|
||||||
|
resources: {
|
||||||
|
limits: {
|
||||||
|
cpu: '1000m',
|
||||||
|
memory: '800Mi'
|
||||||
|
},
|
||||||
|
requests: {
|
||||||
|
cpu: '100m',
|
||||||
|
memory: '20Mi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditAddonLogging',
|
||||||
|
components: {
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
RadioGroup,
|
||||||
|
LabeledInput,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let valuesContentJson = DEFAUL_VALUE;
|
||||||
|
|
||||||
|
try {
|
||||||
|
valuesContentJson = merge({}, DEFAUL_VALUE, jsyaml.load(this.value.spec.valuesContent));
|
||||||
|
} catch (err) {
|
||||||
|
valuesContentJson = DEFAUL_VALUE;
|
||||||
|
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.$store.getters['i18n/t']('generic.notification.title.error'),
|
||||||
|
err: err.data || err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valuesContentJson };
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
valuesContentJson: {
|
||||||
|
handler(neu) {
|
||||||
|
this.$set(this.value.spec, 'valuesContent', jsyaml.dump(neu));
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-if="value.spec.enabled" name="fluentbit" :label="t('harvester.logging.configuration.section.fluentbit')" :weight="-1">
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentbit.resources.requests.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentbit.resources.requests.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentbit.resources.limits.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentbit.resources.limits.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-if="value.spec.enabled" name="fluentd" :label="t('harvester.logging.configuration.section.fluentd')" :weight="-1">
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentd.resources.requests.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentd.resources.requests.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentd.resources.limits.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.fluentd.resources.limits.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
523
pkg/harvester/edit/harvesterhci.io.addon/rancher-monitoring.vue
Normal file
523
pkg/harvester/edit/harvesterhci.io.addon/rancher-monitoring.vue
Normal file
@ -0,0 +1,523 @@
|
|||||||
|
<script>
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { findBy } from '@shell/utils/array';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import LazyImage from '@shell/components/LazyImage';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { ENDPOINTS } from '@shell/config/types';
|
||||||
|
|
||||||
|
const CATTLE_MONITORING_NAMESPACE = 'cattle-monitoring-system';
|
||||||
|
const DEFAUL_VALUE = {
|
||||||
|
prometheus: {
|
||||||
|
prometheusSpec: {
|
||||||
|
resources: {
|
||||||
|
limits: {
|
||||||
|
cpu: '1000m',
|
||||||
|
memory: '3000Mi'
|
||||||
|
},
|
||||||
|
requests: {
|
||||||
|
cpu: '750m',
|
||||||
|
memory: '750Mi'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evaluationInterval: '1m',
|
||||||
|
scrapeInterval: '1m',
|
||||||
|
retention: '5d',
|
||||||
|
retentionSize: '50GiB',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'prometheus-node-exporter': {
|
||||||
|
resources: {
|
||||||
|
limits: {
|
||||||
|
cpu: '200m',
|
||||||
|
memory: '180Mi'
|
||||||
|
},
|
||||||
|
requests: {
|
||||||
|
cpu: '100m',
|
||||||
|
memory: '30Mi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grafana: {
|
||||||
|
resources: {
|
||||||
|
limits: {
|
||||||
|
cpu: '200m',
|
||||||
|
memory: '500Mi'
|
||||||
|
},
|
||||||
|
requests: {
|
||||||
|
cpu: '100m',
|
||||||
|
memory: '200Mi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
alertmanager: {
|
||||||
|
enabled: false,
|
||||||
|
alertmanagerSpec: {
|
||||||
|
retention: '120h',
|
||||||
|
resources: {
|
||||||
|
limits: {
|
||||||
|
cpu: '1000m',
|
||||||
|
memory: '600Mi'
|
||||||
|
},
|
||||||
|
requests: {
|
||||||
|
cpu: '100m',
|
||||||
|
memory: '100Mi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditAddonMonitoring',
|
||||||
|
components: {
|
||||||
|
LabeledInput, RadioGroup, LazyImage, Tabbed, Tab
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const { $store, externalLinks } = this;
|
||||||
|
|
||||||
|
if (!$store.getters['harvester/schemaFor'](ENDPOINTS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = await allHash({ endpoints: $store.dispatch('harvester/findAll', { type: ENDPOINTS }) });
|
||||||
|
|
||||||
|
if (!isEmpty(hash.endpoints)) {
|
||||||
|
const amMatch = externalLinks.alertmanager;
|
||||||
|
const grafanaMatch = externalLinks.grafana;
|
||||||
|
const promeMatch = externalLinks.prometheus;
|
||||||
|
const alertmanager = findBy(
|
||||||
|
hash.endpoints,
|
||||||
|
'id',
|
||||||
|
`${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-alertmanager`
|
||||||
|
);
|
||||||
|
const grafana = findBy(
|
||||||
|
hash.endpoints,
|
||||||
|
'id',
|
||||||
|
`${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-grafana`
|
||||||
|
);
|
||||||
|
const prometheus = findBy(
|
||||||
|
hash.endpoints,
|
||||||
|
'id',
|
||||||
|
`${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-prometheus`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isEmpty(alertmanager) && !isEmpty(alertmanager.subsets)) {
|
||||||
|
amMatch.enabled = true;
|
||||||
|
}
|
||||||
|
if (!isEmpty(grafana) && !isEmpty(grafana.subsets)) {
|
||||||
|
grafanaMatch.enabled = true;
|
||||||
|
}
|
||||||
|
if (!isEmpty(prometheus) && !isEmpty(prometheus.subsets)) {
|
||||||
|
promeMatch.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const grafanaSrc = require('~shell/assets/images/vendor/grafana.svg');
|
||||||
|
const prometheusSrc = require('~shell/assets/images/vendor/prometheus.svg');
|
||||||
|
const currentCluster = this.$store.getters['currentCluster'];
|
||||||
|
let valuesContentJson = DEFAUL_VALUE;
|
||||||
|
|
||||||
|
try {
|
||||||
|
valuesContentJson = merge({}, DEFAUL_VALUE, jsyaml.load(this.value.spec.valuesContent));
|
||||||
|
} catch (err) {
|
||||||
|
valuesContentJson = DEFAUL_VALUE;
|
||||||
|
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.$store.getters['i18n/t']('generic.notification.title.error'),
|
||||||
|
err: err.data || err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valuesContentJson,
|
||||||
|
externalLinks: {
|
||||||
|
alertmanager: {
|
||||||
|
enabled: false,
|
||||||
|
iconSrc: prometheusSrc,
|
||||||
|
label: 'monitoring.overview.linkedList.alertManager.label',
|
||||||
|
description:
|
||||||
|
'monitoring.overview.linkedList.alertManager.description',
|
||||||
|
link: `/k8s/clusters/${ currentCluster.id }/api/v1/namespaces/${ CATTLE_MONITORING_NAMESPACE }/services/http:rancher-monitoring-alertmanager:9093/proxy`,
|
||||||
|
},
|
||||||
|
grafana: {
|
||||||
|
enabled: false,
|
||||||
|
iconSrc: grafanaSrc,
|
||||||
|
label: 'monitoring.overview.linkedList.grafana.label',
|
||||||
|
description: 'monitoring.overview.linkedList.grafana.description',
|
||||||
|
link: `/k8s/clusters/${ currentCluster.id }/api/v1/namespaces/${ CATTLE_MONITORING_NAMESPACE }/services/http:rancher-monitoring-grafana:80/proxy`,
|
||||||
|
},
|
||||||
|
prometheus: {
|
||||||
|
enabled: false,
|
||||||
|
iconSrc: prometheusSrc,
|
||||||
|
label: 'monitoring.overview.linkedList.prometheusPromQl.label',
|
||||||
|
description:
|
||||||
|
'monitoring.overview.linkedList.prometheusPromQl.description',
|
||||||
|
link: `/k8s/clusters/${ currentCluster.id }/api/v1/namespaces/${ CATTLE_MONITORING_NAMESPACE }/services/http:rancher-monitoring-prometheus:9090/proxy`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
prometheusNodeExporter() {
|
||||||
|
return this.valuesContentJson['prometheus-node-exporter'];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
valuesContentJson: {
|
||||||
|
handler(neu) {
|
||||||
|
this.$set(this.value.spec, 'valuesContent', jsyaml.dump(neu));
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-if="value.spec.enabled" name="prometheus" :label="t('harvester.setting.harvesterMonitoring.section.prometheus')" :weight="-1">
|
||||||
|
<a
|
||||||
|
v-clean-tooltip="!externalLinks.prometheus.enabled ? t('monitoring.overview.linkedList.na') : undefined"
|
||||||
|
:disabled="!externalLinks.prometheus.enabled"
|
||||||
|
:href="externalLinks.prometheus.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="subtype-banner m-0 mt-10 mb-10"
|
||||||
|
>
|
||||||
|
<div class="subtype-content">
|
||||||
|
<div class="title">
|
||||||
|
<div class="subtype-logo round-image">
|
||||||
|
<LazyImage :src="externalLinks.prometheus.iconSrc" />
|
||||||
|
</div>
|
||||||
|
<h5>
|
||||||
|
<span>
|
||||||
|
<t :k="externalLinks.prometheus.label" />
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
<div class="flex-right">
|
||||||
|
<i class="icon icon-external-link mr-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.scrapeInterval"
|
||||||
|
:label="t('monitoring.prometheus.config.scrape')"
|
||||||
|
:tooltip="t('harvester.setting.harvesterMonitoring.tips.scrape')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.evaluationInterval"
|
||||||
|
:label="t('monitoring.prometheus.config.evaluation')"
|
||||||
|
:tooltip="t('harvester.setting.harvesterMonitoring.tips.evaluation')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.retention"
|
||||||
|
:label="t('monitoring.prometheus.config.retention')"
|
||||||
|
:tooltip="t('harvester.setting.harvesterMonitoring.tips.retention')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.retentionSize"
|
||||||
|
:label="t('monitoring.prometheus.config.retentionSize')"
|
||||||
|
:tooltip="t('harvester.setting.harvesterMonitoring.tips.retentionSize')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-12 mt-5">
|
||||||
|
<h4 class="mb-0">
|
||||||
|
{{ t('monitoring.prometheus.config.resourceLimits') }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.resources.requests.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.resources.requests.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.resources.limits.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.prometheus.prometheusSpec.resources.limits.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-if="value.spec.enabled" name="nodeExporter" :label="t('harvester.setting.harvesterMonitoring.section.prometheusNodeExporter')" :weight="-2">
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="prometheusNodeExporter.resources.limits.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="prometheusNodeExporter.resources.limits.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="prometheusNodeExporter.resources.requests.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="prometheusNodeExporter.resources.requests.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-if="value.spec.enabled && valuesContentJson.grafana.resources" name="grafana" :label="t('harvester.setting.harvesterMonitoring.section.grafana')" :weight="-3">
|
||||||
|
<a
|
||||||
|
v-clean-tooltip="!externalLinks.grafana.enabled ? t('monitoring.overview.linkedList.na') : undefined"
|
||||||
|
:disabled="!externalLinks.grafana.enabled"
|
||||||
|
:href="externalLinks.grafana.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener nofollow"
|
||||||
|
class="subtype-banner m-0 mt-10 mb-10"
|
||||||
|
>
|
||||||
|
<div class="subtype-content">
|
||||||
|
<div class="title">
|
||||||
|
<div class="subtype-logo round-image">
|
||||||
|
<LazyImage :src="externalLinks.grafana.iconSrc" />
|
||||||
|
</div>
|
||||||
|
<h5>
|
||||||
|
<span>
|
||||||
|
<t :k="externalLinks.grafana.label" />
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
<div class="flex-right">
|
||||||
|
<i class="icon icon-external-link mr-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.grafana.resources.requests.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.grafana.resources.requests.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.grafana.resources.limits.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.grafana.resources.limits.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-if="value.spec.enabled" name="alertmanager" :label="t('harvester.setting.harvesterMonitoring.section.alertmanager')" :weight="-4">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="valuesContentJson.alertmanager.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="valuesContentJson.alertmanager.enabled"
|
||||||
|
v-clean-tooltip="!externalLinks.alertmanager.enabled ? t('monitoring.overview.linkedList.na') : undefined"
|
||||||
|
:disabled="!externalLinks.alertmanager.enabled"
|
||||||
|
:href="externalLinks.alertmanager.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="subtype-banner m-0 mt-10 mb-10"
|
||||||
|
>
|
||||||
|
<div class="subtype-content">
|
||||||
|
<div class="title">
|
||||||
|
<div class="subtype-logo round-image">
|
||||||
|
<LazyImage :src="externalLinks.alertmanager.iconSrc" />
|
||||||
|
</div>
|
||||||
|
<h5>
|
||||||
|
<span>
|
||||||
|
<t :k="externalLinks.alertmanager.label" />
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
<div class="flex-right">
|
||||||
|
<i class="icon icon-external-link mr-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div v-if="valuesContentJson.alertmanager.enabled">
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.alertmanager.alertmanagerSpec.retention"
|
||||||
|
:label="t('monitoring.prometheus.config.retention')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.alertmanager.alertmanagerSpec.resources.limits.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.alertmanager.alertmanagerSpec.resources.limits.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.alertmanager.alertmanagerSpec.resources.requests.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.alertmanager.alertmanagerSpec.resources.requests.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
151
pkg/harvester/edit/harvesterhci.io.addon/rancher-vcluster.vue
Normal file
151
pkg/harvester/edit/harvesterhci.io.addon/rancher-vcluster.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script>
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
|
||||||
|
const DEFAUL_VALUE = {
|
||||||
|
hostname: '',
|
||||||
|
rancherVersion: '',
|
||||||
|
bootstrapPassword: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditAddonVcluster',
|
||||||
|
components: { LabeledInput, RadioGroup },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let valuesContentJson = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
valuesContentJson = merge({}, DEFAUL_VALUE, jsyaml.load(this.value.spec.valuesContent));
|
||||||
|
} catch (err) {
|
||||||
|
valuesContentJson = DEFAUL_VALUE;
|
||||||
|
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.$store.getters['i18n/t']('generic.notification.title.error'),
|
||||||
|
err: err.data || err,
|
||||||
|
}, { root: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valuesContentJson };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
willSave() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!this.value.spec.enabled) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.valuesContentJson.hostname) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherVcluster.hostname') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.valuesContentJson.bootstrapPassword) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.addons.rancherVcluster.password') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
valuesContentJson: {
|
||||||
|
handler(neu) {
|
||||||
|
this.$set(this.value.spec, 'valuesContent', jsyaml.dump(neu));
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="value.spec.enabled">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.hostname"
|
||||||
|
label-key="harvester.addons.rancherVcluster.hostname"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
placeholder="rancher.$vip.nip.io"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.rancherVersion"
|
||||||
|
label-key="harvester.addons.rancherVcluster.rancherVersion"
|
||||||
|
:required="true"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContentJson.bootstrapPassword"
|
||||||
|
label-key="harvester.addons.rancherVcluster.password"
|
||||||
|
:mode="mode"
|
||||||
|
:required="true"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
import { STORAGE_CLASS } from '@shell/config/types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { set, get, clone } from '@shell/utils/object';
|
||||||
|
|
||||||
|
const VALUES_YAML_KEYS = [
|
||||||
|
'resources.requests.cpu',
|
||||||
|
'resources.requests.memory',
|
||||||
|
'resources.limits.cpu',
|
||||||
|
'resources.limits.memory',
|
||||||
|
'pvcClaim.enabled',
|
||||||
|
'pvcClaim.size',
|
||||||
|
'pvcClaim.storageClassName',
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_VALUES = {
|
||||||
|
'resources.requests.cpu': '0.5',
|
||||||
|
'resources.requests.memory': '2Gi',
|
||||||
|
'resources.limits.cpu': '2',
|
||||||
|
'resources.limits.memory': '4Gi',
|
||||||
|
'pvcClaim.enabled': false,
|
||||||
|
'pvcClaim.size': '200Gi',
|
||||||
|
'pvcClaim.storageClassName': '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditHarvesterLogging',
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
RadioGroup,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = { storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }) };
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let valuesObj = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
valuesObj = JSON.parse(this.value?.spec?.valuesContent || '{}');
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
const valuesContent = clone(valuesObj);
|
||||||
|
|
||||||
|
VALUES_YAML_KEYS.map((key) => {
|
||||||
|
if (!get(valuesObj, key)) {
|
||||||
|
set(valuesContent, key, DEFAULT_VALUES[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { valuesContent };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
storageClassOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
|
const out = storages.filter(s => !s.parameters?.backingImage).map((s) => {
|
||||||
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: s.name,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
set(this.value, 'spec.valuesContent', JSON.stringify(this.valuesContent));
|
||||||
|
},
|
||||||
|
|
||||||
|
setDefaultClassName() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find( s => s.isDefault);
|
||||||
|
|
||||||
|
this.$set(this.valuesContent.pvcClaim, 'storageClassName', this.valuesContent?.pvcClaim?.storageClassName || defaultStorage?.metadata?.name || 'longhorn');
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'valuesContent.pvcClaim.enabled'(value) {
|
||||||
|
if (value) {
|
||||||
|
this.setDefaultClassName();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.addons.vmImport.titles.basic')"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="value.spec.enabled">
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContent.resources.limits.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContent.resources.limits.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.limits.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContent.resources.requests.cpu"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.cpu')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContent.resources.requests.memory"
|
||||||
|
:label="t('monitoring.prometheus.config.requests.memory')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<h2>{{ t('harvester.addons.vmImport.titles.pvc') }}</h2>
|
||||||
|
<div v-if="value.spec.enabled">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="valuesContent.pvcClaim.enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="model"
|
||||||
|
:mode="mode"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="valuesContent.pvcClaim.enabled">
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="valuesContent.pvcClaim.size"
|
||||||
|
:label="t('harvester.volume.size')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="valuesContent.pvcClaim.storageClassName"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
:label="t('harvester.storage.storageClass.label')"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@input="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
123
pkg/harvester/edit/harvesterhci.io.cloudtemplate.vue
Normal file
123
pkg/harvester/edit/harvesterhci.io.cloudtemplate.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import YamlEditor from '@shell/components/YamlEditor';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { HCI } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditCloudTemplate',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
YamlEditor,
|
||||||
|
CruResource,
|
||||||
|
LabeledSelect,
|
||||||
|
NameNsDescription,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
config: this.value.data?.cloudInit || '',
|
||||||
|
type: this.value?.metadata?.labels?.[HCI.CLOUD_INIT] || 'user',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
types() {
|
||||||
|
return [{
|
||||||
|
label: 'User Data',
|
||||||
|
value: 'user'
|
||||||
|
}, {
|
||||||
|
label: 'Network Data',
|
||||||
|
value: 'network'
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
this.value.data = { cloudInit: this.config };
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBeforeSave() {
|
||||||
|
if (this.isCreate) {
|
||||||
|
this.value.metadata.labels = {
|
||||||
|
...this.value.metadata.labels,
|
||||||
|
[HCI.CLOUD_INIT]: this.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.value.data = { cloudInit: this.config };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
v-model="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab name="basics" :label="t('harvester.host.tabs.basics')" :weight="1">
|
||||||
|
<div class="mb-20">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="type"
|
||||||
|
:label="t('harvester.cloudTemplate.templateType')"
|
||||||
|
:disabled="!isCreate"
|
||||||
|
:options="types"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resource-yaml">
|
||||||
|
<YamlEditor
|
||||||
|
ref="yamlUser"
|
||||||
|
v-model="config"
|
||||||
|
class="yaml-editor"
|
||||||
|
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||||
|
@onChanges="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$yaml-height: 200px;
|
||||||
|
|
||||||
|
::v-deep .yaml-editor{
|
||||||
|
flex: 1;
|
||||||
|
min-height: $yaml-height;
|
||||||
|
& .code-mirror .CodeMirror {
|
||||||
|
position: initial;
|
||||||
|
height: auto;
|
||||||
|
min-height: $yaml-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
293
pkg/harvester/edit/harvesterhci.io.host/HarvesterDisk.vue
Normal file
293
pkg/harvester/edit/harvesterhci.io.host/HarvesterDisk.vue
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { BadgeState } from '@components/BadgeState';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { RadioGroup, RadioButton } from '@components/Form/Radio';
|
||||||
|
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||||
|
import Tags from '../../components/DiskTags';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { LONGHORN_SYSTEM } from './index';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabelValue,
|
||||||
|
BadgeState,
|
||||||
|
Banner,
|
||||||
|
RadioGroup,
|
||||||
|
RadioButton,
|
||||||
|
Tags,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [
|
||||||
|
HarvesterDisk,
|
||||||
|
],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disks: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'edit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
allowSchedulingOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('generic.enabled'),
|
||||||
|
value: true,
|
||||||
|
}, {
|
||||||
|
label: this.t('generic.disabled'),
|
||||||
|
value: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
evictionRequestedOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('generic.yes'),
|
||||||
|
value: true,
|
||||||
|
}, {
|
||||||
|
label: this.t('generic.no'),
|
||||||
|
value: false,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
mountedMessage() {
|
||||||
|
const state = this.blockDevice?.metadata?.state || {};
|
||||||
|
|
||||||
|
if (state?.error) {
|
||||||
|
return state?.message;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isProvisioned() {
|
||||||
|
return this.blockDevice?.spec.fileSystem.provisioned;
|
||||||
|
},
|
||||||
|
|
||||||
|
forceFormattedDisabled() {
|
||||||
|
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||||
|
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
|
||||||
|
|
||||||
|
const systems = ['ext4', 'XFS'];
|
||||||
|
|
||||||
|
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
|
||||||
|
return true;
|
||||||
|
} else if (systems.includes(fileSystem)) {
|
||||||
|
return false;
|
||||||
|
} else if (!fileSystem) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return !this.canEditPath;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
canEditPath() {
|
||||||
|
if (this.mountedMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.isNew && !this.value.originPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
isFormatted() {
|
||||||
|
return !!this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||||
|
},
|
||||||
|
|
||||||
|
formattedBannerLabel() {
|
||||||
|
const system = this.blockDevice?.status?.deviceStatus?.fileSystem?.type;
|
||||||
|
|
||||||
|
const label = this.t('harvester.host.disk.lastFormattedAt.info');
|
||||||
|
|
||||||
|
if (system) {
|
||||||
|
return `${ label } ${ this.t('harvester.host.disk.fileSystem.info', { system }) }`;
|
||||||
|
} else {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
provisionPhase() {
|
||||||
|
return this.blockDevice?.provisionPhase || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
blockDevice() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const name = this.value?.name;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ name }`) || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
isCorrupted() {
|
||||||
|
return this.blockDevice?.status?.deviceStatus?.fileSystem?.corrupted;
|
||||||
|
},
|
||||||
|
|
||||||
|
isFormatting() {
|
||||||
|
return this.blockDevice.isFormatting;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
this.$emit('input', this.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="disk" @input="update">
|
||||||
|
<div class="mt-10" />
|
||||||
|
<Banner
|
||||||
|
v-if="mountedMessage && isProvisioned"
|
||||||
|
color="error"
|
||||||
|
:label="mountedMessage"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="isFormatting"
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.host.disk.fileSystem.formatting')"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-else-if="isFormatted && !isCorrupted"
|
||||||
|
color="info"
|
||||||
|
:label="formattedBannerLabel"
|
||||||
|
/>
|
||||||
|
<div v-if="!value.isNew">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Tags
|
||||||
|
v-model="value.tags"
|
||||||
|
:label="t('harvester.host.disk.tags.label')"
|
||||||
|
:add-label="t('harvester.host.disk.tags.addLabel')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-12">
|
||||||
|
<div class="pull-right">
|
||||||
|
{{ t('harvester.host.disk.conditions') }}:
|
||||||
|
<BadgeState
|
||||||
|
v-clean-tooltip="readyCondition.message"
|
||||||
|
:color="readyCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||||
|
:icon="readyCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||||
|
label="Ready"
|
||||||
|
class="mr-10 ml-10 state"
|
||||||
|
/>
|
||||||
|
<BadgeState
|
||||||
|
v-clean-tooltip="schedulableCondition.message"
|
||||||
|
:color="schedulableCondition.status === 'True' ? 'bg-success' : 'bg-error' "
|
||||||
|
:icon="schedulableCondition.status === 'True' ? 'icon-checkmark' : 'icon-warning' "
|
||||||
|
label="Schedulable"
|
||||||
|
class="mr-10 state"
|
||||||
|
/>
|
||||||
|
<BadgeState
|
||||||
|
v-if="provisionPhase.label"
|
||||||
|
:color="provisionPhase.color"
|
||||||
|
:icon="provisionPhase.icon"
|
||||||
|
:label="provisionPhase.label"
|
||||||
|
class="mr-10 state"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!value.isNew" class="row mt-30">
|
||||||
|
<div class="col flex span-12">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.storageAvailable.label')"
|
||||||
|
:value="value.storageAvailable"
|
||||||
|
/>
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.storageScheduled.label')"
|
||||||
|
:value="value.storageScheduled"
|
||||||
|
/>
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.disk.storageMaximum.label')"
|
||||||
|
:value="value.storageMaximum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="mt-10" />
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="value.displayName"
|
||||||
|
:label="t('generic.name')"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="(value.isNew && !isFormatted) || isCorrupted" class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.forceFormatted"
|
||||||
|
:mode="mode"
|
||||||
|
name="forceFormatted"
|
||||||
|
label-key="harvester.host.disk.forceFormatted.label"
|
||||||
|
:labels="[t('generic.no'),t('harvester.host.disk.forceFormatted.yes')]"
|
||||||
|
:options="[false, true]"
|
||||||
|
:disabled="forceFormattedDisabled"
|
||||||
|
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
|
||||||
|
>
|
||||||
|
<template #1="{option, listeners}">
|
||||||
|
<RadioButton
|
||||||
|
:label="option.label"
|
||||||
|
:val="option.value"
|
||||||
|
:value="value.forceFormatted"
|
||||||
|
:disabled="forceFormattedDisabled && !value.forceFormatted"
|
||||||
|
v-on="listeners"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.close {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding:0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disk {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.secret-name {
|
||||||
|
height: $input-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-state {
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
216
pkg/harvester/edit/harvesterhci.io.host/HarvesterKsmtuned.vue
Normal file
216
pkg/harvester/edit/harvesterhci.io.host/HarvesterKsmtuned.vue
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
|
export const ksmtunedMode = [{
|
||||||
|
value: 'standard',
|
||||||
|
label: 'Standard'
|
||||||
|
}, {
|
||||||
|
value: 'high',
|
||||||
|
label: 'High-Perfomanace'
|
||||||
|
}, {
|
||||||
|
value: 'customized',
|
||||||
|
label: 'Customized'
|
||||||
|
}];
|
||||||
|
|
||||||
|
export const ksmtunedRunOption = [{
|
||||||
|
label: 'Run',
|
||||||
|
value: 'run'
|
||||||
|
}, {
|
||||||
|
label: 'Stop',
|
||||||
|
value: 'stop'
|
||||||
|
}, {
|
||||||
|
label: 'Prune',
|
||||||
|
value: 'prune'
|
||||||
|
}];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterKsmtuned',
|
||||||
|
components: {
|
||||||
|
Checkbox, LabeledInput, LabeledSelect, RadioGroup, UnitInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.KSTUNED });
|
||||||
|
|
||||||
|
this.ksmtuned = hash.find((node) => {
|
||||||
|
return node.id === this.node.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
|
||||||
|
this.spec = this.ksmtuned.spec;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ksmtuned: {},
|
||||||
|
spec: {},
|
||||||
|
thresCoef: 30,
|
||||||
|
ksmtunedMode,
|
||||||
|
ksmtunedRunOption,
|
||||||
|
enableMergeAcrossNodes: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook(this.saveKsmtuned, 'saveKsmtuned');
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isCustomizedMode() {
|
||||||
|
return this.spec.mode === 'customized';
|
||||||
|
},
|
||||||
|
|
||||||
|
showKsmt() {
|
||||||
|
return this.spec.run === 'run';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveKsmtuned() {
|
||||||
|
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
|
||||||
|
this.$set(this.ksmtuned, 'spec', this.spec);
|
||||||
|
|
||||||
|
await this.ksmtuned.save().catch((reason) => {
|
||||||
|
if (reason?.type === 'error') {
|
||||||
|
this.$store.dispatch('growl/error', {
|
||||||
|
title: this.t('harvester.notification.title.error'),
|
||||||
|
message: reason?.message
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
return Promise.reject(new Error('saveKsmtuned error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="spec.run"
|
||||||
|
:label="t('harvester.host.ksmtuned.run')"
|
||||||
|
:options="ksmtunedRunOption"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="showKsmt">
|
||||||
|
<UnitInput
|
||||||
|
v-model="spec.thresCoef"
|
||||||
|
v-int-number
|
||||||
|
:label="t('harvester.host.ksmtuned.thresCoef')"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox v-model="enableMergeAcrossNodes" :mode="mode" class="check mb-20" type="checkbox" :label="t('harvester.host.ksmtuned.enableMergeNodes')" />
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
<t k="harvester.host.ksmtuned.modeLink" :raw="true" />
|
||||||
|
</h3>
|
||||||
|
<RadioGroup
|
||||||
|
v-model="spec.mode"
|
||||||
|
class="mb-20"
|
||||||
|
:name="t('harvester.host.ksmtuned.mode')"
|
||||||
|
:options="ksmtunedMode"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="isCustomizedMode">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="spec.ksmtunedParameters.boost"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
:label="t('harvester.host.ksmtuned.parameters.boost')"
|
||||||
|
:tooltip="t('harvester.host.ksmtuned.parameters.description.boost')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="spec.ksmtunedParameters.decay"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
:label="t('harvester.host.ksmtuned.parameters.decay')"
|
||||||
|
:tooltip="t('harvester.host.ksmtuned.parameters.description.decay')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="spec.ksmtunedParameters.minPages"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
:label="t('harvester.host.ksmtuned.parameters.minPages')"
|
||||||
|
:tooltip="t('harvester.host.ksmtuned.parameters.description.minPages')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="spec.ksmtunedParameters.maxPages"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
:label="t('harvester.host.ksmtuned.parameters.maxPages')"
|
||||||
|
:tooltip="t('harvester.host.ksmtuned.parameters.description.maxPages')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="spec.ksmtunedParameters.sleepMsec"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
:label="t('harvester.host.ksmtuned.parameters.sleepMsec')"
|
||||||
|
:tooltip="t('harvester.host.ksmtuned.parameters.description.sleepMsec')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
370
pkg/harvester/edit/harvesterhci.io.host/HarvesterSeeder.vue
Normal file
370
pkg/harvester/edit/harvesterhci.io.host/HarvesterSeeder.vue
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
<script>
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
import { base64Encode, base64Decode } from '@shell/utils/crypto';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
|
||||||
|
const _NEW = '_NEW';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterSeeder',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup,
|
||||||
|
ModalWithCard,
|
||||||
|
NameNsDescription,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
registerAfterHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
inventory: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const enableInventory = !!this.inventory?.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
enableInventory,
|
||||||
|
value: this.inventory,
|
||||||
|
secret: {},
|
||||||
|
errors: [],
|
||||||
|
newSecretSelected: false,
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerAfterHook(this.saveInventory, 'saveInventory');
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.secret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: SECRET,
|
||||||
|
data: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
namespace: '',
|
||||||
|
name: '',
|
||||||
|
describe: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
secretOption() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const out = this.$store.getters[`${ inStore }/all`](SECRET).filter((s) => {
|
||||||
|
return s.data?.username && s.data?.password;
|
||||||
|
}).map( (s) => {
|
||||||
|
return {
|
||||||
|
label: s.id,
|
||||||
|
value: s.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (!(this.disableCreate || this.mode === _VIEW) && this.isCreatable) {
|
||||||
|
out.unshift({
|
||||||
|
label: this.t('harvester.virtualMachine.createSSHKey'),
|
||||||
|
value: _NEW,
|
||||||
|
});
|
||||||
|
// }
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedSecret: {
|
||||||
|
get() {
|
||||||
|
const namespace = this.value.spec?.baseboardSpec?.connection?.authSecretRef?.namespace;
|
||||||
|
const name = this.value?.spec?.baseboardSpec?.connection?.authSecretRef?.name;
|
||||||
|
|
||||||
|
if (namespace && name) {
|
||||||
|
return `${ namespace }/${ name }`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
if (value === _NEW) {
|
||||||
|
this.newSecretSelected = true;
|
||||||
|
} else {
|
||||||
|
const [namespace, name] = value.split('/');
|
||||||
|
|
||||||
|
this.$set(this.value.spec.baseboardSpec.connection.authSecretRef, 'namespace', namespace);
|
||||||
|
this.$set(this.value.spec.baseboardSpec.connection.authSecretRef, 'name', name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
username: {
|
||||||
|
get() {
|
||||||
|
return base64Decode(this.secret?.data?.username);
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this.$set(this.secret.data, 'username', base64Encode(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
password: {
|
||||||
|
get() {
|
||||||
|
return base64Decode(this.secret?.data?.password);
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this.$set(this.secret.data, 'password', base64Encode(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveInventory() {
|
||||||
|
if (this.enableInventory) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!this.value.spec.baseboardSpec.connection.host) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.seeder.inventory.host.label') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.value.spec.baseboardSpec.connection.port) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.seeder.inventory.port.label') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selectedSecret) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.seeder.inventory.secret.label') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(exceptionToErrorsArray(errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.value.id) {
|
||||||
|
this.value.metadata.annotations['metal.harvesterhci.io/local-node-name'] = this.node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.metadata.annotations['metal.harvesterhci.io/local-inventory'] = 'true';
|
||||||
|
|
||||||
|
return await this.value.save();
|
||||||
|
} else if (this.value.id) {
|
||||||
|
return await this.value.remove();
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isOpen = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.newSecretSelected = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSecret(buttonCb) {
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
|
if (!this.username) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.username') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.password) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.password') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.secret.save();
|
||||||
|
|
||||||
|
if (res.id) {
|
||||||
|
this.secretOption.push({
|
||||||
|
label: res.id,
|
||||||
|
value: res.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedSecret = res.id;
|
||||||
|
|
||||||
|
buttonCb(true);
|
||||||
|
this.cancel();
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = [err.message];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
newSecretSelected(val) {
|
||||||
|
if (val) {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="inventory.warningMessages.length > 0">
|
||||||
|
<Banner
|
||||||
|
v-for="msg in inventory.warningMessages"
|
||||||
|
:key="msg.text"
|
||||||
|
color="error"
|
||||||
|
:label="msg.text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="enableInventory"
|
||||||
|
:options="[
|
||||||
|
{ label: t('generic.enabled'), value: true },
|
||||||
|
{ label: t('generic.disabled'), value: false }
|
||||||
|
]"
|
||||||
|
:mode="mode"
|
||||||
|
name="enableInventory"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="enableInventory">
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="value.spec.baseboardSpec.connection.host"
|
||||||
|
:label="t('harvester.seeder.inventory.host.label')"
|
||||||
|
:placeholder="t('harvester.seeder.inventory.host.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
v-model="value.spec.baseboardSpec.connection.insecureTLS"
|
||||||
|
class="mt-5"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.seeder.inventory.insecureTLS.label')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model.number="value.spec.baseboardSpec.connection.port"
|
||||||
|
:label="t('harvester.seeder.inventory.port.label')"
|
||||||
|
:placeholder="t('harvester.seeder.inventory.port.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="selectedSecret"
|
||||||
|
:label="t('harvester.seeder.inventory.secret.label')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="secretOption"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.spec.events.enabled"
|
||||||
|
name="enabled"
|
||||||
|
:options="[true, false]"
|
||||||
|
:label="t('harvester.seeder.inventory.event.label')"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="value.spec.events.enabled"
|
||||||
|
class="col span-6"
|
||||||
|
>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="value.spec.events.pollingInterval"
|
||||||
|
:label="t('harvester.seeder.inventory.pollingInterval.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalWithCard
|
||||||
|
v-if="isOpen"
|
||||||
|
width="80%"
|
||||||
|
:errors="errors"
|
||||||
|
name="secretModal"
|
||||||
|
@finish="saveSecret"
|
||||||
|
@close="cancel"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ t('harvester.seeder.inventory.secret.create.title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<NameNsDescription
|
||||||
|
v-model="secret"
|
||||||
|
:namespaced="true"
|
||||||
|
mode="create"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="username"
|
||||||
|
:label="t('harvester.virtualMachine.input.username')"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
:label="t('harvester.virtualMachine.input.password')"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ModalWithCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
688
pkg/harvester/edit/harvesterhci.io.host/index.vue
Normal file
688
pkg/harvester/edit/harvesterhci.io.host/index.vue
Normal file
@ -0,0 +1,688 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import Footer from '@shell/components/form/Footer';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
||||||
|
import ButtonDropdown from '@shell/components/ButtonDropdown';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { LONGHORN, SECRET } from '@shell/config/types';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { formatSi } from '@shell/utils/units';
|
||||||
|
import { findBy } from '@shell/utils/array';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
import Loading from '@shell/components/Loading.vue';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
|
||||||
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||||
|
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import HarvesterDisk from './HarvesterDisk';
|
||||||
|
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||||
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
|
import Tags from '../../components/DiskTags';
|
||||||
|
|
||||||
|
export const LONGHORN_SYSTEM = 'longhorn-system';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditNode',
|
||||||
|
components: {
|
||||||
|
Footer,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
LabeledInput,
|
||||||
|
NameNsDescription,
|
||||||
|
ArrayListGrouped,
|
||||||
|
HarvesterDisk,
|
||||||
|
HarvesterKsmtuned,
|
||||||
|
ButtonDropdown,
|
||||||
|
KeyValue,
|
||||||
|
Banner,
|
||||||
|
Tags,
|
||||||
|
Loading,
|
||||||
|
HarvesterSeeder,
|
||||||
|
MessageLink,
|
||||||
|
},
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||||
|
blockDevices: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BLOCK_DEVICE }),
|
||||||
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
|
secrets: this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.INVENTORY)) {
|
||||||
|
hash.inventories = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.INVENTORY });
|
||||||
|
}
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
|
||||||
|
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
||||||
|
const provisionedBlockDevices = blockDevices.filter((d) => {
|
||||||
|
const provisioned = d?.spec?.fileSystem?.provisioned;
|
||||||
|
const isCurrentNode = d?.spec?.nodeName === this.value.id;
|
||||||
|
const isLonghornMounted = findBy(this.longhornDisks, 'name', d.metadata.name);
|
||||||
|
|
||||||
|
return provisioned && isCurrentNode && !isLonghornMounted;
|
||||||
|
})
|
||||||
|
.map((d) => {
|
||||||
|
const corrupted = d?.status?.deviceStatus?.fileSystem?.corrupted;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isNew: true,
|
||||||
|
name: d?.metadata?.name,
|
||||||
|
originPath: d?.spec?.fileSystem?.mountPoint,
|
||||||
|
path: d?.spec?.fileSystem?.mountPoint,
|
||||||
|
blockDevice: d,
|
||||||
|
displayName: d?.displayName,
|
||||||
|
forceFormatted: corrupted ? true : d?.spec?.fileSystem?.forceFormatted || false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const disks = [...this.longhornDisks, ...provisionedBlockDevices];
|
||||||
|
|
||||||
|
this.disks = disks;
|
||||||
|
this.newDisks = clone(disks);
|
||||||
|
this.blockDeviceOpts = this.getBlockDeviceOpts();
|
||||||
|
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||||
|
const seeder = addons.find(addon => addon.id === `harvester-system/${ ADD_ONS.HARVESTER_SEEDER }`);
|
||||||
|
|
||||||
|
const seederEnabled = seeder ? seeder?.spec?.enabled : false;
|
||||||
|
|
||||||
|
if (seederEnabled) {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const inventories = this.$store.getters[`${ inStore }/all`](HCI.INVENTORY) || [];
|
||||||
|
|
||||||
|
const inventory = inventories.find(inv => inv.id === `harvester-system/${ this.value.id }`);
|
||||||
|
|
||||||
|
if (inventory) {
|
||||||
|
this.inventory = await this.$store.dispatch(`${ inStore }/clone`, { resource: inventory });
|
||||||
|
} else {
|
||||||
|
this.inventory = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.INVENTORY,
|
||||||
|
metadata: {
|
||||||
|
name: this.value.id,
|
||||||
|
namespace: 'harvester-system'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inventory.applyDefaults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const customName = this.value.metadata?.annotations?.[HCI_LABELS_ANNOTATIONS.HOST_CUSTOM_NAME] || '';
|
||||||
|
const consoleUrl = this.value.metadata?.annotations?.[HCI_LABELS_ANNOTATIONS.HOST_CONSOLE_URL] || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
customName,
|
||||||
|
consoleUrl,
|
||||||
|
disks: [],
|
||||||
|
newDisks: [],
|
||||||
|
blockDevice: [],
|
||||||
|
blockDeviceOpts: [],
|
||||||
|
filteredLabels: clone(this.value.filteredSystemLabels),
|
||||||
|
inventory: {},
|
||||||
|
originValue: clone(this.value),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
removedDisks() {
|
||||||
|
const out = this.disks.filter((d) => {
|
||||||
|
return !findBy(this.newDisks, 'name', d.name);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
longhornDisks() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const longhornNode = this.$store.getters[`${ inStore }/byId`](LONGHORN.NODES, `${ LONGHORN_SYSTEM }/${ this.value.id }`);
|
||||||
|
const diskStatus = longhornNode?.status?.diskStatus || {};
|
||||||
|
const diskSpec = longhornNode?.spec?.disks || {};
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
increment: 1024,
|
||||||
|
minExponent: 3,
|
||||||
|
maxExponent: 3,
|
||||||
|
maxPrecision: 2,
|
||||||
|
suffix: 'iB',
|
||||||
|
};
|
||||||
|
|
||||||
|
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||||
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ key }`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...diskStatus[key],
|
||||||
|
...diskSpec?.[key],
|
||||||
|
name: key,
|
||||||
|
isNew: false,
|
||||||
|
storageReserved: formatSi(diskSpec[key]?.storageReserved, formatOptions),
|
||||||
|
storageAvailable: formatSi(diskStatus[key]?.storageAvailable, formatOptions),
|
||||||
|
storageMaximum: formatSi(diskStatus[key]?.storageMaximum, formatOptions),
|
||||||
|
storageScheduled: formatSi(diskStatus[key]?.storageScheduled, formatOptions),
|
||||||
|
blockDevice,
|
||||||
|
displayName: blockDevice?.displayName || key,
|
||||||
|
forceFormatted: blockDevice?.spec?.fileSystem?.forceFormatted || false,
|
||||||
|
tags: diskSpec?.[key]?.tags || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return longhornDisks;
|
||||||
|
},
|
||||||
|
|
||||||
|
showFormattedWarning() {
|
||||||
|
const out = this.newDisks.filter(d => d.forceFormatted && d.isNew) || [];
|
||||||
|
|
||||||
|
return out.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasKsmtunedSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasBlockDevicesSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.BLOCK_DEVICE);
|
||||||
|
},
|
||||||
|
|
||||||
|
longhornNode() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const longhornNodes = this.$store.getters[`${ inStore }/all`](LONGHORN.NODES);
|
||||||
|
|
||||||
|
return longhornNodes.find(node => node.id === `${ LONGHORN_SYSTEM }/${ this.value.id }`);
|
||||||
|
},
|
||||||
|
|
||||||
|
seederEnabled() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||||
|
const seeder = addons.find(addon => addon.id === `harvester-system/${ ADD_ONS.HARVESTER_SEEDER }`);
|
||||||
|
|
||||||
|
return seeder ? seeder?.spec?.enabled : false;
|
||||||
|
},
|
||||||
|
|
||||||
|
toEnableSeederAddon() {
|
||||||
|
const { cluster } = this.$router?.currentRoute?.params || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-namespace-id`,
|
||||||
|
params: {
|
||||||
|
resource: `${ HCI.ADD_ONS }`,
|
||||||
|
namespace: 'harvester-system',
|
||||||
|
cluster,
|
||||||
|
id: `${ ADD_ONS.HARVESTER_SEEDER }`
|
||||||
|
},
|
||||||
|
query: { mode: _EDIT }
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
hasAddonSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/schemaFor`](HCI.ADD_ONS);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasSeederAddon() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const addons = this.$store.getters[`${ inStore }/all`](HCI.ADD_ONS);
|
||||||
|
|
||||||
|
return addons.find(addon => addon.id === `harvester-system/${ ADD_ONS.HARVESTER_SEEDER }`);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasInventorySchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/schemaFor`](HCI.INVENTORY);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
customName(neu) {
|
||||||
|
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.HOST_CUSTOM_NAME, neu);
|
||||||
|
},
|
||||||
|
|
||||||
|
consoleUrl(neu) {
|
||||||
|
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.HOST_CONSOLE_URL, neu);
|
||||||
|
},
|
||||||
|
|
||||||
|
newDisks() {
|
||||||
|
this.blockDeviceOpts = this.getBlockDeviceOpts();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.registerAfterHook) {
|
||||||
|
this.registerAfterHook(this.saveDisk);
|
||||||
|
this.registerAfterHook(this.saveLonghornNode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addDisk(id) {
|
||||||
|
const removedDisk = findBy(this.removedDisks, 'blockDevice.id', id);
|
||||||
|
|
||||||
|
if (removedDisk) {
|
||||||
|
return this.newDisks.push(removedDisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const disk = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, id);
|
||||||
|
const mountPoint = disk?.spec?.fileSystem?.mountPoint;
|
||||||
|
const lastFormattedAt = disk?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||||
|
|
||||||
|
let forceFormatted = true;
|
||||||
|
const systems = ['ext4', 'XFS'];
|
||||||
|
|
||||||
|
if (disk.childParts?.length > 0) {
|
||||||
|
forceFormatted = true;
|
||||||
|
} else if (lastFormattedAt) {
|
||||||
|
forceFormatted = false;
|
||||||
|
} else if (systems.includes(disk?.status?.deviceStatus?.fileSystem?.type)) {
|
||||||
|
forceFormatted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = disk?.metadata?.name;
|
||||||
|
|
||||||
|
this.newDisks.push({
|
||||||
|
name,
|
||||||
|
path: mountPoint,
|
||||||
|
allowScheduling: false,
|
||||||
|
evictionRequested: false,
|
||||||
|
storageReserved: 0,
|
||||||
|
isNew: true,
|
||||||
|
originPath: disk?.spec?.fileSystem?.mountPoint,
|
||||||
|
blockDevice: disk,
|
||||||
|
displayName: disk?.displayName,
|
||||||
|
forceFormatted,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveDisk() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const addDisks = this.newDisks.filter(d => d.isNew);
|
||||||
|
const removeDisks = this.disks.filter(d => !findBy(this.newDisks, 'name', d.name) && d.blockDevice);
|
||||||
|
|
||||||
|
if (addDisks.length === 0 && removeDisks.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else if (addDisks.length !== 0 && removeDisks.length === 0) {
|
||||||
|
const updatedDisks = addDisks.filter((d) => {
|
||||||
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||||
|
const { provisioned, forceFormatted } = blockDevice.spec.fileSystem;
|
||||||
|
|
||||||
|
if (provisioned && forceFormatted === d.forceFormatted) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedDisks.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(addDisks.map((d) => {
|
||||||
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||||
|
|
||||||
|
blockDevice.spec.fileSystem.provisioned = true;
|
||||||
|
blockDevice.spec.fileSystem.forceFormatted = d.forceFormatted;
|
||||||
|
|
||||||
|
return blockDevice.save();
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Promise.all(removeDisks.map((d) => {
|
||||||
|
const blockDevice = this.$store.getters[`${ inStore }/byId`](HCI.BLOCK_DEVICE, `${ LONGHORN_SYSTEM }/${ d.name }`);
|
||||||
|
|
||||||
|
blockDevice.spec.fileSystem.provisioned = false;
|
||||||
|
|
||||||
|
return blockDevice.save();
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.host.disk.notification.success', { name: this.value.metadata?.name || '' }),
|
||||||
|
}, { root: true });
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(exceptionToErrorsArray(err));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
canRemove(row) {
|
||||||
|
return !!row?.value?.blockDevice;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove(scope) {
|
||||||
|
scope.remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHostLabels(labels) {
|
||||||
|
this.filteredLabels = labels;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectable(opt) {
|
||||||
|
if ( opt.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
getBlockDeviceOpts() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const blockDevices = this.$store.getters[`${ inStore }/all`](HCI.BLOCK_DEVICE);
|
||||||
|
|
||||||
|
const out = blockDevices
|
||||||
|
.filter((d) => {
|
||||||
|
const addedToNodeCondition = findBy(d?.status?.conditions || [], 'type', 'AddedToNode');
|
||||||
|
const isAdded = findBy(this.newDisks, 'name', d.metadata.name);
|
||||||
|
const isRemoved = findBy(this.removedDisks, 'name', d.metadata.name);
|
||||||
|
|
||||||
|
const deviceType = d.status?.deviceStatus?.details?.deviceType;
|
||||||
|
|
||||||
|
if (deviceType !== 'disk' || d?.status?.state !== 'Active') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!findBy(this.disks || [], 'name', d.metadata.name) &&
|
||||||
|
d?.spec?.nodeName === this.value.id &&
|
||||||
|
(!addedToNodeCondition || addedToNodeCondition?.status === 'False') &&
|
||||||
|
!d.spec?.fileSystem?.provisioned &&
|
||||||
|
!isAdded) ||
|
||||||
|
isRemoved
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map((d) => {
|
||||||
|
const devPath = d.spec?.devPath;
|
||||||
|
const deviceType = d.status?.deviceStatus?.details?.deviceType;
|
||||||
|
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
|
||||||
|
const size = formatSi(sizeBytes, { increment: 1024 });
|
||||||
|
const parentDevice = d.status?.deviceStatus?.parentDevice;
|
||||||
|
const isChildAdded = this.newDisks.find(newDisk => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
|
||||||
|
const name = d.displayName;
|
||||||
|
|
||||||
|
let label = `${ name } (Type: ${ deviceType }, Size: ${ size })`;
|
||||||
|
|
||||||
|
if (parentDevice) {
|
||||||
|
label = `- ${ label }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: d.id,
|
||||||
|
action: this.addDisk,
|
||||||
|
kind: !parentDevice ? 'group' : '',
|
||||||
|
disabled: !!isChildAdded,
|
||||||
|
group: parentDevice || devPath,
|
||||||
|
isParent: !!parentDevice,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortBy(out, ['group', 'isParent', 'label']);
|
||||||
|
},
|
||||||
|
|
||||||
|
ddButtonAction() {
|
||||||
|
this.blockDeviceOpts = this.getBlockDeviceOpts();
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
const filteredLabels = this.filteredLabels || {};
|
||||||
|
|
||||||
|
this.value.metadata.labels = {
|
||||||
|
...this.value.metadata.labels,
|
||||||
|
...filteredLabels,
|
||||||
|
};
|
||||||
|
|
||||||
|
const originLabels = this.value.filteredSystemLabels;
|
||||||
|
|
||||||
|
Object.keys(originLabels).map((key) => {
|
||||||
|
if (!filteredLabels[key]) {
|
||||||
|
delete this.value.metadata.labels[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveLonghornNode() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const disks = this.longhornNode?.spec?.disks || {};
|
||||||
|
|
||||||
|
this.newDisks.map((disk) => {
|
||||||
|
(disks[disk.name] || {}).tags = disk.tags;
|
||||||
|
});
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const retrySave = async() => {
|
||||||
|
try {
|
||||||
|
await this.longhornNode.save();
|
||||||
|
} catch (err) {
|
||||||
|
if ((err.status === 409 || err.status === 403) && count < 3) {
|
||||||
|
count++;
|
||||||
|
|
||||||
|
await this.$store.dispatch(`${ inStore }/find`, {
|
||||||
|
type: LONGHORN.NODES,
|
||||||
|
id: this.longhornNode.id,
|
||||||
|
opt: { force: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, '5000'));
|
||||||
|
await retrySave();
|
||||||
|
} else {
|
||||||
|
return Promise.reject(exceptionToErrorsArray(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await retrySave();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else id="node">
|
||||||
|
<div class="content">
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:namespaced="false"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<Tabbed ref="tabbed" class="mt-15" :side-tabs="true">
|
||||||
|
<Tab name="basics" :weight="100" :label="t('harvester.host.tabs.basics')">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="customName"
|
||||||
|
:label="t('harvester.host.detail.customName')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model="consoleUrl"
|
||||||
|
:label="t('harvester.host.detail.consoleUrl')"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="hasBlockDevicesSchema"
|
||||||
|
name="disk"
|
||||||
|
:weight="80"
|
||||||
|
:label="t('harvester.host.tabs.storage')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="longhornNode"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<Tags
|
||||||
|
v-model="longhornNode.spec.tags"
|
||||||
|
:label="t('harvester.host.tags.label')"
|
||||||
|
:add-label="t('harvester.host.tags.addLabel')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrayListGrouped
|
||||||
|
v-model="newDisks"
|
||||||
|
:mode="mode"
|
||||||
|
:initial-empty-row="false"
|
||||||
|
>
|
||||||
|
<template #default="props">
|
||||||
|
<HarvesterDisk
|
||||||
|
v-model="props.row.value"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:disks="disks"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #add>
|
||||||
|
<ButtonDropdown
|
||||||
|
v-if="!isView"
|
||||||
|
:button-label="t('harvester.host.disk.add')"
|
||||||
|
:dropdown-options="blockDeviceOpts"
|
||||||
|
size="sm"
|
||||||
|
:selectable="selectable"
|
||||||
|
@click-action="e=>addDisk(e.value)"
|
||||||
|
@dd-button-action="ddButtonAction"
|
||||||
|
>
|
||||||
|
<template #option="option">
|
||||||
|
<template v-if="option.kind === 'group'">
|
||||||
|
<b>
|
||||||
|
{{ option.label }}
|
||||||
|
</b>
|
||||||
|
</template>
|
||||||
|
<div v-else>
|
||||||
|
{{ option.label }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ButtonDropdown>
|
||||||
|
</template>
|
||||||
|
<template #remove-button="scope">
|
||||||
|
<button
|
||||||
|
v-if="canRemove(scope.row, scope.i) && !isView"
|
||||||
|
type="button"
|
||||||
|
class="btn role-link close btn-sm"
|
||||||
|
@click="() => onRemove(scope)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<span v-else />
|
||||||
|
</template>
|
||||||
|
</ArrayListGrouped>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-if="hasKsmtunedSchema" name="Ksmtuned" :weight="70" :label="t('harvester.host.tabs.ksmtuned')">
|
||||||
|
<HarvesterKsmtuned :mode="mode" :node="value" :register-before-hook="registerBeforeHook" />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="hasAddonSchema"
|
||||||
|
name="seeder"
|
||||||
|
:weight="60"
|
||||||
|
:label="t('harvester.host.tabs.seeder')"
|
||||||
|
>
|
||||||
|
<HarvesterSeeder
|
||||||
|
v-if="seederEnabled && hasInventorySchema"
|
||||||
|
:mode="mode"
|
||||||
|
:node="value"
|
||||||
|
:register-after-hook="registerAfterHook"
|
||||||
|
:inventory="inventory"
|
||||||
|
/>
|
||||||
|
<div v-else-if="seederEnabled && !hasInventorySchema">
|
||||||
|
<Banner
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.seeder.banner.noInventory')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Banner
|
||||||
|
v-if="hasSeederAddon"
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
|
<MessageLink
|
||||||
|
:to="toEnableSeederAddon"
|
||||||
|
prefix-label="harvester.seeder.banner.enable.prefix"
|
||||||
|
middle-label="harvester.seeder.banner.enable.middle"
|
||||||
|
suffix-label="harvester.seeder.banner.enable.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-else
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.seeder.banner.noAddon')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab name="labels" label-key="harvester.host.tabs.labels">
|
||||||
|
<KeyValue
|
||||||
|
key="labels"
|
||||||
|
:value="filteredLabels"
|
||||||
|
:add-label="t('labels.addLabel')"
|
||||||
|
:mode="mode"
|
||||||
|
:title="t('labels.labels.title')"
|
||||||
|
:read-allowed="false"
|
||||||
|
:value-can-be-empty="true"
|
||||||
|
@input="updateHostLabels"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
<Banner
|
||||||
|
v-if="showFormattedWarning"
|
||||||
|
color="warning"
|
||||||
|
:label="t('harvester.host.disk.forceFormatted.toolTip')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer class="footer" :mode="mode" :errors="errors" @save="save" @done="done" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#node {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nicOption {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
112
pkg/harvester/edit/harvesterhci.io.keypair.vue
Normal file
112
pkg/harvester/edit/harvesterhci.io.keypair.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
|
||||||
|
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditKeypair',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
FileSelector,
|
||||||
|
NameNsDescription
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
if ( !this.value.spec ) {
|
||||||
|
this.value.spec = {};
|
||||||
|
this.value.metadata = { name: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: this.value.spec.publicKey || '',
|
||||||
|
randomString: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
publicKey(neu) {
|
||||||
|
const trimNeu = neu.trim();
|
||||||
|
|
||||||
|
this.value.spec.publicKey = trimNeu;
|
||||||
|
|
||||||
|
const splitSSH = trimNeu.split(/\s+/);
|
||||||
|
|
||||||
|
if (splitSSH.length === 3 && !this.value.metadata.name) {
|
||||||
|
const keyComment = splitSSH[2];
|
||||||
|
|
||||||
|
this.randomString = randomStr(10).toLowerCase();
|
||||||
|
this.value.metadata.name = keyComment.includes('@') ? keyComment.split('@')[0] : keyComment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: { onKeySelected: createOnSelected('publicKey') },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="save"
|
||||||
|
>
|
||||||
|
<div class="header mb-20">
|
||||||
|
<FileSelector
|
||||||
|
v-if="isCreate"
|
||||||
|
class="btn btn-sm bg-primary mt-10"
|
||||||
|
:label="t('generic.readFromFile')"
|
||||||
|
accept=".pub"
|
||||||
|
@selected="onKeySelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
:key="randomString"
|
||||||
|
v-model="value"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||||
|
<Tab name="basic" :label="t('harvester.sshKey.tabs.basics')" :weight="1" class="bordered-table">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="publicKey"
|
||||||
|
type="multiline"
|
||||||
|
:mode="mode"
|
||||||
|
:min-height="160"
|
||||||
|
:label="t('harvester.sshKey.keypair')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
pkg/harvester/edit/harvesterhci.io.logging.clusterflow.vue
Normal file
25
pkg/harvester/edit/harvesterhci.io.logging.clusterflow.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
import { LAST_NAMESPACE } from '@shell/store/prefs';
|
||||||
|
import { LOGGING } from '@shell/config/types';
|
||||||
|
import Flow from '@shell/edit/logging-flow';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: Flow,
|
||||||
|
created() {
|
||||||
|
if (this.isCreate && this.value.type === LOGGING.CLUSTER_FLOW) {
|
||||||
|
this.value.metadata.namespace = 'cattle-logging-system';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
this.registerAfterHook(() => {
|
||||||
|
const allNamespaces = this.$store.getters['allNamespaces'];
|
||||||
|
const defaultNamepsace = allNamespaces.find(N => N.id === 'default');
|
||||||
|
const ns = defaultNamepsace?.id || allNamespaces?.[0]?.id || '';
|
||||||
|
|
||||||
|
this.value.$dispatch('prefs/set', { key: LAST_NAMESPACE, value: ns }, { root: true });
|
||||||
|
this.willSave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
21
pkg/harvester/edit/harvesterhci.io.logging.clusteroutput.vue
Normal file
21
pkg/harvester/edit/harvesterhci.io.logging.clusteroutput.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import { LAST_NAMESPACE } from '@shell/store/prefs';
|
||||||
|
import CreateEditView from './harvesterhci.io.logging.output';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: CreateEditView,
|
||||||
|
created() {
|
||||||
|
if (this.isCreate) {
|
||||||
|
this.value.metadata.namespace = 'cattle-logging-system';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerAfterHook(() => {
|
||||||
|
const allNamespaces = this.$store.getters['allNamespaces'];
|
||||||
|
const defaultNamepsace = allNamespaces.find(N => N.id === 'default');
|
||||||
|
const ns = defaultNamepsace?.id || allNamespaces?.[0]?.id || '';
|
||||||
|
|
||||||
|
this.value.$dispatch('prefs/set', { key: LAST_NAMESPACE, value: ns }, { root: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
7
pkg/harvester/edit/harvesterhci.io.logging.flow.vue
Normal file
7
pkg/harvester/edit/harvesterhci.io.logging.flow.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import Flow from '@shell/edit/logging-flow';
|
||||||
|
export default { components: { Flow } };
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Flow v-bind="$attrs" />
|
||||||
|
</template>
|
||||||
345
pkg/harvester/edit/harvesterhci.io.logging.output.vue
Normal file
345
pkg/harvester/edit/harvesterhci.io.logging.output.vue
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { SECRET, LOGGING, SCHEMA } from '@shell/config/types';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import Labels from '@shell/components/form/Labels';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { PROVIDERS } from '@shell/models/logging.banzaicloud.io.output';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import { clone, set } from '@shell/utils/object';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { createYaml } from '@shell/utils/create-yaml';
|
||||||
|
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
|
||||||
|
import { FLOW_TYPE } from '../config/harvester-map';
|
||||||
|
|
||||||
|
const LOGGING_EVENT = 'Logging/Event';
|
||||||
|
const AUDIT_ONLY = 'Audit Only';
|
||||||
|
const OUTPUT_TYPE = [LOGGING_EVENT, AUDIT_ONLY];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Banner, CruResource, Labels, LabeledSelect, NameNsDescription, Tab, Tabbed, YamlEditor
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: SECRET });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const schemas = this.$store.getters['harvester/all'](SCHEMA);
|
||||||
|
|
||||||
|
if (this.isCreate) {
|
||||||
|
this.value.metadata.namespace = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
set(this.value, 'spec', this.value.spec || {});
|
||||||
|
|
||||||
|
const providers = PROVIDERS.map(provider => ({
|
||||||
|
...provider,
|
||||||
|
value: provider.name,
|
||||||
|
label: this.t(provider.labelKey)
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (this.mode !== _VIEW) {
|
||||||
|
this.$set(this.value, 'spec', this.value.spec || {});
|
||||||
|
|
||||||
|
providers.forEach((provider) => {
|
||||||
|
this.$set(this.value.spec, provider.name, this.value.spec[provider.name] || clone(provider.default));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProviders = providers.filter((provider) => {
|
||||||
|
const specProvider = this.value.spec[provider.name];
|
||||||
|
const correctedSpecProvider = provider.name === 'forward' ? specProvider?.servers?.[0] || {} : specProvider;
|
||||||
|
|
||||||
|
return !isEmpty(correctedSpecProvider) && !isEqual(correctedSpecProvider, provider.default);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedProvider = selectedProviders?.[0]?.value || providers[0].value;
|
||||||
|
|
||||||
|
let bufferYaml;
|
||||||
|
|
||||||
|
if ( !isEmpty(this.value.spec[selectedProvider]?.buffer) ) {
|
||||||
|
bufferYaml = jsyaml.dump(this.value.spec[selectedProvider].buffer);
|
||||||
|
} else {
|
||||||
|
bufferYaml = createYaml(schemas, `logging.banzaicloud.io.v1beta1.output.spec.${ selectedProvider }.buffer`, []);
|
||||||
|
// createYaml doesn't support passing reference types (array, map) as the first type. As such
|
||||||
|
// I'm manipulating the output since I'm not sure it's something we want to actually support
|
||||||
|
// seeing as it's really createResourceYaml and this here is a gray area between spoofed types
|
||||||
|
// and just a field within a spec.
|
||||||
|
bufferYaml = bufferYaml.substring(bufferYaml.indexOf('\n') + 1).replaceAll('# ', '#');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bufferYaml,
|
||||||
|
initialBufferYaml: bufferYaml,
|
||||||
|
providers,
|
||||||
|
selectedProvider,
|
||||||
|
hasMultipleProvidersSelected: selectedProviders.length > 1,
|
||||||
|
selectedProviders,
|
||||||
|
LOGGING,
|
||||||
|
loggingType: this.value.loggingType !== FLOW_TYPE.AUDIT ? LOGGING_EVENT : AUDIT_ONLY
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
EDITOR_MODES() {
|
||||||
|
return EDITOR_MODES;
|
||||||
|
},
|
||||||
|
enabledProviders() {
|
||||||
|
return this.providers.filter(p => p.enabled);
|
||||||
|
},
|
||||||
|
cruMode() {
|
||||||
|
if (this.selectedProviders.length > 1 || !this.value.allProvidersSupported) {
|
||||||
|
return _VIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mode;
|
||||||
|
},
|
||||||
|
outputTypeOptions() {
|
||||||
|
return OUTPUT_TYPE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getComponent(name) {
|
||||||
|
return require(`@shell/edit/logging.banzaicloud.io.output/providers/${ name }`).default;
|
||||||
|
},
|
||||||
|
launch(provider) {
|
||||||
|
this.$refs.tabbed.select(provider.name);
|
||||||
|
},
|
||||||
|
willSave() {
|
||||||
|
this.value.spec = { [this.selectedProvider]: this.value.spec[this.selectedProvider] };
|
||||||
|
|
||||||
|
const bufferJson = jsyaml.load(this.bufferYaml);
|
||||||
|
|
||||||
|
if (!isEmpty(bufferJson)) {
|
||||||
|
this.value.spec[this.selectedProvider].buffer = bufferJson;
|
||||||
|
} else {
|
||||||
|
this.$delete(this.value.spec[this.selectedProvider], 'buffer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.loggingType === AUDIT_ONLY) {
|
||||||
|
this.$set(this.value.spec, 'loggingRef', 'harvester-kube-audit-log-ref');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabChanged({ tab }) {
|
||||||
|
if ( tab.name === 'buffer' ) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if ( this.$refs.yaml ) {
|
||||||
|
this.$refs.yaml.refresh();
|
||||||
|
this.$refs.yaml.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onYamlEditorReady(cm) {
|
||||||
|
cm.getMode().fold = 'yamlcomments';
|
||||||
|
cm.execCommand('foldAll');
|
||||||
|
cm.execCommand('unfold');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="output">
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="cruMode"
|
||||||
|
:resource="value"
|
||||||
|
:subtypes="[]"
|
||||||
|
:validation-passed="true"
|
||||||
|
:errors="errors"
|
||||||
|
:can-yaml="true"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
v-if="!isView"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
label="generic.name"
|
||||||
|
:register-before-hook="registerBeforeHook"
|
||||||
|
:namespaced="value.type !== LOGGING.CLUSTER_OUTPUT"
|
||||||
|
/>
|
||||||
|
<Banner v-if="selectedProviders.length > 1" color="info">
|
||||||
|
{{ t('logging.output.tips.singleProvider') }}
|
||||||
|
</Banner>
|
||||||
|
<Banner v-else-if="!value.allProvidersSupported" color="info">
|
||||||
|
{{ t('logging.output.tips.multipleProviders') }}
|
||||||
|
</Banner>
|
||||||
|
<Tabbed v-else ref="tabbed" :side-tabs="true" @changed="tabChanged($event)">
|
||||||
|
<Tab name="Output" label="Output" :weight="2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="loggingType"
|
||||||
|
class="mb-20"
|
||||||
|
:options="outputTypeOptions"
|
||||||
|
:disabled="!isCreate"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('generic.type')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect v-model="selectedProvider" label="Output" :options="providers" :mode="mode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<component :is="getComponent(selectedProvider)" :value="value.spec[selectedProvider]" :namespace="value.namespace" :mode="mode" />
|
||||||
|
</Tab>
|
||||||
|
<Tab name="buffer" :label="t('logging.output.buffer.label')" :weight="1">
|
||||||
|
<YamlEditor
|
||||||
|
ref="yaml"
|
||||||
|
v-model="bufferYaml"
|
||||||
|
:scrolling="false"
|
||||||
|
:initial-yaml-values="initialBufferYaml"
|
||||||
|
:editor-mode="isView ? EDITOR_MODES.VIEW_CODE : EDITOR_MODES.EDIT_CODE"
|
||||||
|
@onReady="onYamlEditorReady"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="!isView"
|
||||||
|
name="labels-and-annotations"
|
||||||
|
label-key="generic.labelsAndAnnotations"
|
||||||
|
:weight="0"
|
||||||
|
>
|
||||||
|
<Labels
|
||||||
|
default-container-class="labels-and-annotations-container"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:display-side-by-side="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
$chart: 110px;
|
||||||
|
$side: 15px;
|
||||||
|
$margin: 10px;
|
||||||
|
$logo: 60px;
|
||||||
|
|
||||||
|
.output {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
.provider {
|
||||||
|
h1 {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0 -1*$margin;
|
||||||
|
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-4')) {
|
||||||
|
.toggle-gradient-box {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-7')) {
|
||||||
|
.toggle-gradient-box {
|
||||||
|
width: calc(50% - 2 * #{$margin});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-9')) {
|
||||||
|
.toggle-gradient-box {
|
||||||
|
width: calc(33.33333% - 2 * #{$margin});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-12')) {
|
||||||
|
.toggle-gradient-box {
|
||||||
|
width: calc(25% - 2 * #{$margin});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-gradient-box {
|
||||||
|
margin: $margin;
|
||||||
|
padding: $margin;
|
||||||
|
position: relative;
|
||||||
|
border-radius: calc( 1.5 * var(--border-radius));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 30px var(--shadow);
|
||||||
|
transition: box-shadow 0.1s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-label {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
min-width: calc(1.5 * var(--border-radius));
|
||||||
|
width: $side;
|
||||||
|
border-top-right-radius: calc( 1.5 * var(--border-radius));
|
||||||
|
border-bottom-right-radius: calc( 1.5 * var(--border-radius));
|
||||||
|
|
||||||
|
label {
|
||||||
|
text-align: center;
|
||||||
|
writing-mode: tb;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 2px;
|
||||||
|
display: block;
|
||||||
|
white-space: no-wrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
width: $logo;
|
||||||
|
height: $logo;
|
||||||
|
border-radius: calc(2 * var(--border-radius));
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: white;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: $logo - 4px;
|
||||||
|
height: $logo - 4px;
|
||||||
|
object-fit: contain;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-position: right center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,278 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { MONITORING } from '@shell/config/types';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { EDITOR_MODES } from '@shell/components/YamlEditor';
|
||||||
|
import { RECEIVERS_TYPES } from '@shell/models/monitoring.coreos.com.receiver';
|
||||||
|
import RouteConfig from '@shell/edit/monitoring.coreos.com.alertmanagerconfig/routeConfig';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import ActionMenu from '@shell/components/ActionMenu';
|
||||||
|
import { _CREATE, _EDIT, _VIEW, _CONFIG } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ActionMenu,
|
||||||
|
CruResource,
|
||||||
|
Loading,
|
||||||
|
NameNsDescription,
|
||||||
|
ResourceTable,
|
||||||
|
RouteConfig,
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const alertmanagerConfigId = this.value.id;
|
||||||
|
|
||||||
|
const alertmanagerConfigResource = await this.$store.dispatch(`${ inStore }/find`, { type: MONITORING.ALERTMANAGERCONFIG, id: alertmanagerConfigId });
|
||||||
|
|
||||||
|
this.alertmanagerConfigId = alertmanagerConfigId;
|
||||||
|
this.alertmanagerConfigResource = alertmanagerConfigResource;
|
||||||
|
this.alertmanagerConfigDetailRoute = alertmanagerConfigResource._detailLocation;
|
||||||
|
|
||||||
|
const alertmanagerConfigActions = alertmanagerConfigResource.availableActions;
|
||||||
|
const receiverActions = alertmanagerConfigResource.getReceiverActions(alertmanagerConfigActions);
|
||||||
|
|
||||||
|
this.receiverActions = receiverActions;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
this.value.applyDefaults();
|
||||||
|
|
||||||
|
const defaultReceiverValues = {};
|
||||||
|
const receiverSchema = this.$store.getters['harvester/schemaFor'](MONITORING.SPOOFED.ALERTMANAGERCONFIG_RECEIVER_SPEC);
|
||||||
|
const routeSchema = this.$store.getters['harvester/schemaFor'](MONITORING.SPOOFED.ALERTMANAGERCONFIG_ROUTE_SPEC);
|
||||||
|
const receiverOptions = (this.value?.spec?.receivers || []).map((receiver) => receiver.name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
actionMenuTargetElement: null,
|
||||||
|
actionMenuTargetEvent: null,
|
||||||
|
config: _CONFIG,
|
||||||
|
create: _CREATE,
|
||||||
|
createReceiverLink: this.value.getCreateReceiverRoute(),
|
||||||
|
defaultReceiverValues,
|
||||||
|
receiverActionMenuIsOpen: false,
|
||||||
|
receiverTableHeaders: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
labelKey: 'tableHeaders.name',
|
||||||
|
value: 'name',
|
||||||
|
sort: ['nameSort'],
|
||||||
|
formatter: 'LinkDetail',
|
||||||
|
canBeVariable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
labelKey: 'tableHeaders.type',
|
||||||
|
value: 'name',
|
||||||
|
formatter: 'ReceiverIcons',
|
||||||
|
canBeVariable: true,
|
||||||
|
}
|
||||||
|
// Add more columns
|
||||||
|
],
|
||||||
|
newReceiverType: null,
|
||||||
|
receiverActions: [],
|
||||||
|
receiverOptions,
|
||||||
|
receiverTypes: RECEIVERS_TYPES,
|
||||||
|
routeSchema,
|
||||||
|
receiverSchema,
|
||||||
|
selectedReceiverName: '',
|
||||||
|
selectedRowValue: null,
|
||||||
|
view: _VIEW,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
editorMode() {
|
||||||
|
if ( this.mode === _VIEW ) {
|
||||||
|
return EDITOR_MODES.VIEW_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDITOR_MODES.EDIT_CODE;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
translateReceiverTypes() {
|
||||||
|
return this.receiverTypes.map((receiverType) => {
|
||||||
|
return {
|
||||||
|
...receiverType,
|
||||||
|
label: this.t(receiverType.label)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getReceiverDetailLink(receiverData) {
|
||||||
|
if (receiverData && receiverData.name) {
|
||||||
|
return this.value.getReceiverDetailLink(receiverData.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleReceiverActionMenu() {
|
||||||
|
this.receiverActionMenuIsOpen = true;
|
||||||
|
},
|
||||||
|
setActionMenuState(eventData) {
|
||||||
|
// This method is called when the user clicks a context menu
|
||||||
|
// for a receiver in the receiver in the receiver list view.
|
||||||
|
// It sets the target element so the menu can open where the
|
||||||
|
// user clicked.
|
||||||
|
const { event, targetElement } = eventData;
|
||||||
|
|
||||||
|
// TargetElement could be an array of more than
|
||||||
|
// one if there is more than one ref of the same name.
|
||||||
|
if (event && targetElement) {
|
||||||
|
this.actionMenuTargetElement = targetElement;
|
||||||
|
this.actionMenuTargetEvent = event;
|
||||||
|
|
||||||
|
// We take the selected receiver name out of the target
|
||||||
|
// element's ID to help us build the URL to link to
|
||||||
|
// the detail page of that receiver.
|
||||||
|
// We use a plus sign as the delimiter to separate the
|
||||||
|
// name because the plus is not an allowed character in
|
||||||
|
// Kubernetes names.
|
||||||
|
this.selectedReceiverName = targetElement.id.split('+').slice(2).join('');
|
||||||
|
|
||||||
|
this.toggleReceiverActionMenu();
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not find action menu target element.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goToEdit() {
|
||||||
|
// 'goToEdit' is the exact name of an action for AlertmanagerConfig
|
||||||
|
// and this method executes the action.
|
||||||
|
this.$router.push(this.alertmanagerConfigResource.getEditReceiverConfigRoute(this.selectedReceiverName, _EDIT));
|
||||||
|
},
|
||||||
|
|
||||||
|
goToEditYaml() {
|
||||||
|
// 'goToEditYaml' is the exact name of an action for AlertmanagerConfig
|
||||||
|
// and this method executes the action.
|
||||||
|
this.$router.push(this.alertmanagerConfigResource.getEditReceiverYamlRoute(this.selectedReceiverName, _EDIT));
|
||||||
|
},
|
||||||
|
promptRemove() {
|
||||||
|
// 'promptRemove' is the exact name of an action for AlertmanagerConfig
|
||||||
|
// and this method executes the action.
|
||||||
|
// Get the name of the receiver to delete from the action info.
|
||||||
|
const nameOfReceiverToDelete = this.selectedReceiverName;
|
||||||
|
// Remove it from the configuration of the parent AlertmanagerConfig
|
||||||
|
// resource.
|
||||||
|
const existingReceivers = this.alertmanagerConfigResource.spec.receivers || [];
|
||||||
|
const receiversMinusDeletedItem = existingReceivers.filter((receiver) => {
|
||||||
|
return receiver.name !== nameOfReceiverToDelete;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.alertmanagerConfigResource.spec.receivers = receiversMinusDeletedItem;
|
||||||
|
// After saving the AlertmanagerConfig, the resource has been deleted.
|
||||||
|
this.alertmanagerConfigResource.save(...arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
class="route"
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:errors="errors"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:subtypes="[]"
|
||||||
|
:cancel-event="true"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
v-model="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="isNamespaced"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed>
|
||||||
|
<Tab
|
||||||
|
:label="t('monitoring.route.label')"
|
||||||
|
:weight="1"
|
||||||
|
name="route"
|
||||||
|
>
|
||||||
|
<RouteConfig
|
||||||
|
:value="value.spec.route"
|
||||||
|
:mode="mode"
|
||||||
|
:receiver-options="receiverOptions"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
:label="t('alertmanagerConfigReceiver.receivers')"
|
||||||
|
:weight="2"
|
||||||
|
name="receivers"
|
||||||
|
>
|
||||||
|
<ResourceTable
|
||||||
|
:headers="receiverTableHeaders"
|
||||||
|
:schema="receiverSchema"
|
||||||
|
:rows="value.spec.receivers || []"
|
||||||
|
:get-custom-detail-link="getReceiverDetailLink"
|
||||||
|
:table-actions="false"
|
||||||
|
:custom-actions="value.receiverActions"
|
||||||
|
@clickedActionButton="setActionMenuState"
|
||||||
|
>
|
||||||
|
<template #header-button>
|
||||||
|
<router-link
|
||||||
|
v-if="createReceiverLink && createReceiverLink.name"
|
||||||
|
:to="mode !== create ? createReceiverLink : {}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn role-primary"
|
||||||
|
:disabled="mode === create"
|
||||||
|
:tooltip="t('monitoring.alertmanagerConfig.disabledReceiverButton')"
|
||||||
|
>
|
||||||
|
{{ t('monitoring.receiver.addReceiver') }}
|
||||||
|
<i
|
||||||
|
v-if="mode === create"
|
||||||
|
v-clean-tooltip="t('monitoring.alertmanagerConfig.disabledReceiverButton')"
|
||||||
|
class="icon icon-info"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</ResourceTable>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
<ActionMenu
|
||||||
|
:custom-actions="receiverActions"
|
||||||
|
:open="receiverActionMenuIsOpen"
|
||||||
|
:use-custom-target-element="true"
|
||||||
|
:custom-target-element="actionMenuTargetElement"
|
||||||
|
:custom-target-event="actionMenuTargetEvent"
|
||||||
|
@close="receiverActionMenuIsOpen = false"
|
||||||
|
@goToEdit="goToEdit"
|
||||||
|
@goToEditYaml="goToEditYaml"
|
||||||
|
@promptRemove="promptRemove"
|
||||||
|
/>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
h3 {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.route {
|
||||||
|
&[real-mode=view] .label {
|
||||||
|
color: var(--input-label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,282 @@
|
|||||||
|
<script>
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const AUTO = 'auto';
|
||||||
|
const MANUAL = 'manual';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
NameNsDescription,
|
||||||
|
RadioGroup,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const config = JSON.parse(this.value.spec.config);
|
||||||
|
const annotations = this.value?.metadata?.annotations || {};
|
||||||
|
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||||
|
|
||||||
|
if ((config.bridge || '').endsWith('-br')) {
|
||||||
|
config.bridge = config.bridge.slice(0, -3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this.value.vlanType || 'L2VlanNetwork' ;
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
type,
|
||||||
|
layer3Network: {
|
||||||
|
mode: layer3Network.mode || AUTO,
|
||||||
|
serverIPAddr: layer3Network.serverIPAddr || '',
|
||||||
|
cidr: layer3Network.cidr || '',
|
||||||
|
gateway: layer3Network.gateway || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }) });
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.updateBeforeSave);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
modeOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||||
|
value: AUTO,
|
||||||
|
}, {
|
||||||
|
label: this.t('harvester.network.layer3Network.mode.manual'),
|
||||||
|
value: MANUAL,
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
|
||||||
|
return clusterNetworks.map((n) => {
|
||||||
|
const disabled = !n.isReady;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: disabled ? `${ n.id } (${ this.t('generic.notReady') })` : n.id,
|
||||||
|
value: n.id,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
networkType() {
|
||||||
|
return ['L2VlanNetwork', 'UntaggedNetwork'];
|
||||||
|
},
|
||||||
|
|
||||||
|
isUntaggedNetwork() {
|
||||||
|
if (this.isView) {
|
||||||
|
return this.value.vlanType === 'UntaggedNetwork';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === 'UntaggedNetwork';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveNetwork(buttonCb) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.bridge) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.layer3Network.mode === MANUAL) {
|
||||||
|
if (!this.layer3Network.gateway) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||||
|
}
|
||||||
|
if (!this.layer3Network.cidr) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||||
|
|
||||||
|
await this.save(buttonCb);
|
||||||
|
},
|
||||||
|
|
||||||
|
input(neu) {
|
||||||
|
const pattern = /^([1-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-9][0-4])$/;
|
||||||
|
|
||||||
|
if (!pattern.test(neu) && neu !== '') {
|
||||||
|
this.config.vlan = neu > 4094 ? 4094 : 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBeforeSave() {
|
||||||
|
this.config.name = this.value.metadata.name;
|
||||||
|
|
||||||
|
if (this.isUntaggedNetwork) {
|
||||||
|
delete this.config.vlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.spec.config = JSON.stringify({
|
||||||
|
...this.config,
|
||||||
|
bridge: `${ this.config.bridge }-br`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="saveNetwork"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
v-model="value"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||||
|
<Tab name="basics" :label="t('harvester.network.tabs.basics')" :weight="99" class="bordered-table">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="type"
|
||||||
|
class="mb-20"
|
||||||
|
:options="networkType"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.fields.type')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="!isUntaggedNetwork"
|
||||||
|
v-model.number="config.vlan"
|
||||||
|
v-int-number
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g. 1-4094"
|
||||||
|
:label="t('tableHeaders.networkVlan')"
|
||||||
|
:mode="mode"
|
||||||
|
@input="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div
|
||||||
|
class="col span-12"
|
||||||
|
>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="config.bridge"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.network.clusterNetwork.label')"
|
||||||
|
required
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="!isUntaggedNetwork"
|
||||||
|
name="layer3Network"
|
||||||
|
:label="t('harvester.network.tabs.layer3Network')"
|
||||||
|
:weight="98"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="layer3Network.mode"
|
||||||
|
name="layer3NetworkMode"
|
||||||
|
:label="t('harvester.network.layer3Network.mode.label')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="modeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="layer3Network.mode === 'auto'"
|
||||||
|
class="row mt-10"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="layer3Network.serverIPAddr"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.network.layer3Network.serverIPAddr.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="row mt-10"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="layer3Network.cidr"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.network.layer3Network.cidr.label')"
|
||||||
|
:placeholder="t('harvester.network.layer3Network.cidr.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="layer3Network.gateway"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.network.layer3Network.gateway.label')"
|
||||||
|
:placeholder="t('harvester.network.layer3Network.gateway.placeholder')"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
281
pkg/harvester/edit/harvesterhci.io.secret.vue
Normal file
281
pkg/harvester/edit/harvesterhci.io.secret.vue
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<script>
|
||||||
|
import { SECRET_TYPES as TYPES } from '@shell/config/secret';
|
||||||
|
import { MANAGEMENT, NAMESPACE, DEFAULT_WORKSPACE } from '@shell/config/types';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import {
|
||||||
|
CLOUD_CREDENTIAL, _CLONE, _CREATE, _EDIT, _FLAGGED
|
||||||
|
} from '@shell/config/query-params';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import Labels from '@shell/components/form/Labels';
|
||||||
|
import { HIDE_SENSITIVE, LAST_NAMESPACE } from '@shell/store/prefs';
|
||||||
|
import { CAPI } from '@shell/config/labels-annotations';
|
||||||
|
import { clear } from '@shell/utils/array';
|
||||||
|
import { importCloudCredential } from '@shell/utils/dynamic-importer';
|
||||||
|
import SelectIconGrid from '@shell/components/SelectIconGrid';
|
||||||
|
import { ucFirst } from '@shell/utils/string';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CruSecret',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
Loading,
|
||||||
|
NameNsDescription,
|
||||||
|
CruResource,
|
||||||
|
Tabbed,
|
||||||
|
Tab,
|
||||||
|
Labels,
|
||||||
|
SelectIconGrid
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
if ( this.isCloud ) {
|
||||||
|
this.nodeDrivers = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE_DRIVER });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const newCloudCred = this.$route.query[CLOUD_CREDENTIAL] === _FLAGGED;
|
||||||
|
const editCloudCred = this.mode === _EDIT && this.value._type === TYPES.CLOUD_CREDENTIAL;
|
||||||
|
const cloneCloudCred = this.realMode === _CLONE && this.liveValue._type === TYPES.CLOUD_CREDENTIAL;
|
||||||
|
const isCloud = newCloudCred || editCloudCred || cloneCloudCred;
|
||||||
|
|
||||||
|
if ( newCloudCred ) {
|
||||||
|
this.value.metadata.namespace = DEFAULT_WORKSPACE;
|
||||||
|
|
||||||
|
this.$set(this.value.metadata, 'name', '');
|
||||||
|
|
||||||
|
this.$set(this.value, 'data', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretTypes = [
|
||||||
|
{
|
||||||
|
label: 'Custom',
|
||||||
|
value: 'custom'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'divider',
|
||||||
|
disabled: true,
|
||||||
|
kind: 'divider'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
Object.values(TYPES).forEach((t) => {
|
||||||
|
secretTypes.push({
|
||||||
|
label: t,
|
||||||
|
value: t
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if ( this.mode === _CREATE ) {
|
||||||
|
this.$set(this.value, '_type', TYPES.OPAQUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCloud,
|
||||||
|
nodeDrivers: null,
|
||||||
|
secretTypes,
|
||||||
|
secretType: this.value._type,
|
||||||
|
initialSecretType: this.value._type
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
typeKey() {
|
||||||
|
if ( this.isCloud ) {
|
||||||
|
return 'cloud';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( this.value._type ) {
|
||||||
|
case TYPES.TLS: return 'tls';
|
||||||
|
case TYPES.BASIC: return 'basic';
|
||||||
|
case TYPES.DOCKER_JSON: return 'registry';
|
||||||
|
case TYPES.SSH: return 'ssh';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'generic';
|
||||||
|
},
|
||||||
|
|
||||||
|
dataComponent() {
|
||||||
|
return require(`@shell/edit/secret/${ this.typeKey }`).default;
|
||||||
|
},
|
||||||
|
|
||||||
|
driverName() {
|
||||||
|
const driver = this.value.metadata?.annotations?.[CAPI.CREDENTIAL_DRIVER];
|
||||||
|
|
||||||
|
return driver;
|
||||||
|
},
|
||||||
|
|
||||||
|
cloudComponent() {
|
||||||
|
const driver = this.driverName;
|
||||||
|
const haveProviders = this.$store.getters['plugins/credentialDrivers'];
|
||||||
|
|
||||||
|
if ( haveProviders.includes(driver) ) {
|
||||||
|
return importCloudCredential(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
return importCloudCredential('generic');
|
||||||
|
},
|
||||||
|
|
||||||
|
namespaces() {
|
||||||
|
return this.$store.getters['cluster/all'](NAMESPACE).map((obj) => {
|
||||||
|
return {
|
||||||
|
label: obj.nameDisplay,
|
||||||
|
value: obj.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hideSensitiveData() {
|
||||||
|
return this.$store.getters['prefs/get'](HIDE_SENSITIVE);
|
||||||
|
},
|
||||||
|
|
||||||
|
dataLabel() {
|
||||||
|
switch (this.value._type) {
|
||||||
|
case TYPES.TLS:
|
||||||
|
return this.t('secret.certificate.certificate');
|
||||||
|
case TYPES.SSH:
|
||||||
|
return this.t('secret.ssh.keys');
|
||||||
|
case TYPES.BASIC:
|
||||||
|
return this.t('secret.authentication');
|
||||||
|
default:
|
||||||
|
return this.t('secret.data');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerAfterHook(() => {
|
||||||
|
const allNamespaces = this.$store.getters['allNamespaces'];
|
||||||
|
const defaultNamepsace = allNamespaces.find(N => N.id === 'default');
|
||||||
|
const ns = defaultNamepsace?.id || allNamespaces?.[0]?.id || '';
|
||||||
|
|
||||||
|
this.value.$dispatch('prefs/set', { key: LAST_NAMESPACE, value: ns }, { root: true });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveSecret(btnCb) {
|
||||||
|
if ( this.errors ) {
|
||||||
|
clear(this.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof this.$refs.cloudComponent?.test === 'function' ) {
|
||||||
|
try {
|
||||||
|
const res = await this.$refs.cloudComponent.test();
|
||||||
|
|
||||||
|
if ( !res || res?.errors) {
|
||||||
|
if (res?.errors) {
|
||||||
|
this.errors = res.errors;
|
||||||
|
} else {
|
||||||
|
this.errors = ['Authentication test failed, please check your credentials'];
|
||||||
|
}
|
||||||
|
btnCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = [e];
|
||||||
|
btnCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.save(btnCb);
|
||||||
|
},
|
||||||
|
|
||||||
|
typeDisplay(type, driver) {
|
||||||
|
if ( type === CAPI.CREDENTIAL_DRIVER ) {
|
||||||
|
return this.$store.getters['i18n/withFallback'](`cluster.provider."${ driver }"`, null, driver);
|
||||||
|
} else {
|
||||||
|
const fallback = type.replace(/^kubernetes.io\//, '');
|
||||||
|
|
||||||
|
return this.$store.getters['i18n/withFallback'](`secret.types."${ type }"`, null, fallback);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initialDisplayFor(type) {
|
||||||
|
const fallback = (ucFirst(this.typeDisplay(type) || '').replace(/[^A-Z]/g, '') || type).substr(0, 3);
|
||||||
|
|
||||||
|
return this.$store.getters['i18n/withFallback'](`secret.initials."${ type }"`, null, fallback);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectCustomType(type) {
|
||||||
|
if (type !== 'custom') {
|
||||||
|
this.$set(this.value, '_type', type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="filled-height">
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:mode="mode"
|
||||||
|
:validation-passed="true"
|
||||||
|
:selected-subtype="value._type"
|
||||||
|
:resource="value"
|
||||||
|
:errors="errors"
|
||||||
|
@finish="saveSecret"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
v-model="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="!isCloud"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="spacer" />
|
||||||
|
<component
|
||||||
|
:is="cloudComponent"
|
||||||
|
v-if="isCloud"
|
||||||
|
ref="cloudComponent"
|
||||||
|
:driver-name="driverName"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:hide-sensitive-data="hideSensitiveData"
|
||||||
|
/>
|
||||||
|
<Tabbed
|
||||||
|
v-else
|
||||||
|
:side-tabs="true"
|
||||||
|
default-tab="data"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="data"
|
||||||
|
:label="dataLabel"
|
||||||
|
:weight="99"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="dataComponent"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:hide-sensitive-data="hideSensitiveData"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="labels"
|
||||||
|
label-key="generic.labelsAndAnnotations"
|
||||||
|
:weight="-1"
|
||||||
|
>
|
||||||
|
<Labels
|
||||||
|
v-model="value"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
255
pkg/harvester/edit/harvesterhci.io.setting.vue
Normal file
255
pkg/harvester/edit/harvesterhci.io.setting.vue
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup,
|
||||||
|
TextAreaAutoGrow
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const t = this.$store.getters['i18n/t'];
|
||||||
|
const setting =
|
||||||
|
HCI_ALLOWED_SETTINGS[this.value.id] ||
|
||||||
|
HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
|
||||||
|
|
||||||
|
let enumOptions = [];
|
||||||
|
|
||||||
|
if (setting.kind === 'enum') {
|
||||||
|
enumOptions = setting.options.map(id => ({
|
||||||
|
label: `advancedSettings.enum.harv-${ this.value.id }.${ id }`,
|
||||||
|
value: id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const canReset =
|
||||||
|
setting.canReset || !!this.value.default || this.value.canReset;
|
||||||
|
|
||||||
|
if (this.value.value === undefined) {
|
||||||
|
this.$set(this.value, 'value', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.value = this.value.value || this.value.default || '';
|
||||||
|
const oldValue = this.value.value;
|
||||||
|
|
||||||
|
const isHarvester = this.value?.type?.includes('harvesterhci');
|
||||||
|
|
||||||
|
// Get all the custom volume types from the file names of this folder
|
||||||
|
const customSettingComponents = require
|
||||||
|
.context('../components/settings', false, /^.*\.vue$/)
|
||||||
|
.keys()
|
||||||
|
.map(path => path.replace(/(\.\/)|(.vue)/g, ''));
|
||||||
|
|
||||||
|
return {
|
||||||
|
setting,
|
||||||
|
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||||
|
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||||
|
enumOptions,
|
||||||
|
canReset,
|
||||||
|
errors: [],
|
||||||
|
hasCustomComponent: false,
|
||||||
|
customComponent: null,
|
||||||
|
customSettingComponents,
|
||||||
|
oldValue
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
doneLocationOverride() {
|
||||||
|
return this.value.doneOverride;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
let customComponent = false;
|
||||||
|
const hasCustomComponent = this.customSettingComponents.includes(this.value.id);
|
||||||
|
|
||||||
|
if ( hasCustomComponent ) {
|
||||||
|
try {
|
||||||
|
customComponent = require(`../components/settings/${ this.value.id }.vue`).default;
|
||||||
|
} catch {}
|
||||||
|
} else {
|
||||||
|
// Some resources like vlan and network go out to a non-standard location (edit/<resource>/<id>.vue). For example
|
||||||
|
// 'edit/harvesterhci.io.managedchart/rancher-monitoring.vue'
|
||||||
|
const resource = this.$route.params.resource;
|
||||||
|
const name = this.value.metadata.name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
customComponent = require(`./${ resource }/${ name }.vue`).default;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasCustomComponent = !!customComponent;
|
||||||
|
this.customComponent = customComponent;
|
||||||
|
|
||||||
|
this.registerAfterHook(() => {
|
||||||
|
if (this.value.id === HCI_SETTING.RANCHER_MANAGER_SUPPORT) {
|
||||||
|
this.$store.commit('isRancherInHarvester', this.value.value === 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
done() {
|
||||||
|
this.$router.go(-1);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSettings(done) {
|
||||||
|
const t = this.$store.getters['i18n/t'];
|
||||||
|
|
||||||
|
// Validate the JSON if the setting is a json value
|
||||||
|
if (this.setting.kind === 'json' && this.value.default) {
|
||||||
|
try {
|
||||||
|
JSON.parse(this.value.value);
|
||||||
|
this.errors = [];
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = [t('advancedSettings.edit.invalidJSON')];
|
||||||
|
|
||||||
|
return done(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.metadata.name === HCI_SETTING.CLUSTER_REGISTRATION_URL && this.oldValue && this.value.value !== this.oldValue) {
|
||||||
|
await this.clusterRegistrationUrlTip();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save(done);
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterRegistrationUrlTip() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$store.dispatch('harvester/promptModal', {
|
||||||
|
component: 'MessageBox',
|
||||||
|
callback: (action) => {
|
||||||
|
if (action === 'ok') {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentKey: 'harvester.setting.clusterRegistrationUrl.message'
|
||||||
|
}, { root: true });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault(ev) {
|
||||||
|
// Lose the focus on the button after click
|
||||||
|
if (ev && ev.srcElement) {
|
||||||
|
ev.srcElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.id === HCI_SETTING.VLAN) {
|
||||||
|
this.value.enable = false;
|
||||||
|
if (this.value.config) {
|
||||||
|
this.value.config.defaultPhysicalNIC = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.value.value = this.value.default || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
||||||
|
this.$refs.settingComp.useDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
class="route"
|
||||||
|
:errors="errors"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:subtypes="[]"
|
||||||
|
:can-yaml="false"
|
||||||
|
:cancel-event="true"
|
||||||
|
@error="e => (errors = e)"
|
||||||
|
@finish="saveSettings"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<h4 v-clean-html="description"></h4>
|
||||||
|
|
||||||
|
<h5 v-if="editHelp" v-clean-html="editHelp" class="edit-help" />
|
||||||
|
|
||||||
|
<div class="edit-change mt-20">
|
||||||
|
<h5 v-t="'advancedSettings.edit.changeSetting'" />
|
||||||
|
<button
|
||||||
|
:disabled="!canReset"
|
||||||
|
type="button"
|
||||||
|
class="btn role-primary"
|
||||||
|
@click="useDefault"
|
||||||
|
>
|
||||||
|
{{ t('advancedSettings.edit.useDefault') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20">
|
||||||
|
<div v-if="setting.from === 'import'">
|
||||||
|
<component
|
||||||
|
:is="customComponent"
|
||||||
|
v-if="hasCustomComponent"
|
||||||
|
ref="settingComp"
|
||||||
|
v-model="value"
|
||||||
|
:register-before-hook="registerBeforeHook"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="setting.kind === 'enum'">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="value.value"
|
||||||
|
:label="t('advancedSettings.edit.value')"
|
||||||
|
:localized-label="true"
|
||||||
|
:mode="mode"
|
||||||
|
:options="enumOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="setting.kind === 'boolean'">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.value"
|
||||||
|
name="settings_value"
|
||||||
|
:labels="[
|
||||||
|
t('advancedSettings.edit.trueOption'),
|
||||||
|
t('advancedSettings.edit.falseOption')
|
||||||
|
]"
|
||||||
|
:options="['true', 'false']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="setting.kind === 'multiline' || setting.kind === 'json'">
|
||||||
|
<TextAreaAutoGrow v-model="value.value" :min-height="254" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<LabeledInput
|
||||||
|
v-model="value.value"
|
||||||
|
:label="t('advancedSettings.edit.value')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.edit-change {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> h5 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .edit-help code {
|
||||||
|
padding: 1px 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
324
pkg/harvester/edit/harvesterhci.io.storage/index.vue
Normal file
324
pkg/harvester/edit/harvesterhci.io.storage/index.vue
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import Tags from '../../components/DiskTags';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
|
||||||
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||||
|
import { STORAGE_CLASS, LONGHORN } from '@shell/config/types';
|
||||||
|
import { CSI_DRIVER } from '../../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
|
||||||
|
const LONGHORN_DRIVER = 'driver.longhorn.io';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterStorage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ArrayList,
|
||||||
|
CruResource,
|
||||||
|
LabeledSelect,
|
||||||
|
LabeledInput,
|
||||||
|
NameNsDescription,
|
||||||
|
RadioGroup,
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
Loading,
|
||||||
|
Tags,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const reclaimPolicyOptions = [{
|
||||||
|
label: this.t('storageClass.customize.reclaimPolicy.delete'),
|
||||||
|
value: 'Delete'
|
||||||
|
}, {
|
||||||
|
label: this.t('storageClass.customize.reclaimPolicy.retain'),
|
||||||
|
value: 'Retain'
|
||||||
|
}];
|
||||||
|
|
||||||
|
const allowVolumeExpansionOptions = [
|
||||||
|
{
|
||||||
|
label: this.t('generic.enabled'),
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.t('generic.disabled'),
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const volumeBindingModeOptions = [
|
||||||
|
{
|
||||||
|
label: this.t('storageClass.customize.volumeBindingMode.now'),
|
||||||
|
value: 'Immediate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.t('harvester.storage.customize.volumeBindingMode.later'),
|
||||||
|
value: 'WaitForFirstConsumer'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedTopologies = clone(this.value.allowedTopologies?.[0]?.matchLabelExpressions || []);
|
||||||
|
|
||||||
|
this.$set(this.value, 'parameters', this.value.parameters || {});
|
||||||
|
this.$set(this.value, 'provisioner', this.value.provisioner || LONGHORN_DRIVER);
|
||||||
|
this.$set(this.value, 'allowVolumeExpansion', this.value.allowVolumeExpansion || allowVolumeExpansionOptions[0].value);
|
||||||
|
this.$set(this.value, 'reclaimPolicy', this.value.reclaimPolicy || reclaimPolicyOptions[0].value);
|
||||||
|
this.$set(this.value, 'volumeBindingMode', this.value.volumeBindingMode || volumeBindingModeOptions[0].value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reclaimPolicyOptions,
|
||||||
|
allowVolumeExpansionOptions,
|
||||||
|
volumeBindingModeOptions,
|
||||||
|
mountOptions: [],
|
||||||
|
provisioner: LONGHORN_DRIVER,
|
||||||
|
STORAGE_CLASS,
|
||||||
|
allowedTopologies,
|
||||||
|
defaultAddValue: {
|
||||||
|
key: '',
|
||||||
|
values: [],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
|
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||||
|
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showUnsupportedStorage: mapFeature(UNSUPPORTED_STORAGE_DRIVERS),
|
||||||
|
|
||||||
|
inStore() {
|
||||||
|
return this.$store.getters['currentProduct'].inStore;
|
||||||
|
},
|
||||||
|
|
||||||
|
modeOverride() {
|
||||||
|
return this.isCreate ? _CREATE : _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
provisionerWatch() {
|
||||||
|
return this.value.provisioner;
|
||||||
|
},
|
||||||
|
|
||||||
|
provisioners() {
|
||||||
|
const csiDrivers = this.$store.getters[`${ this.inStore }/all`](CSI_DRIVER) || [];
|
||||||
|
const format = { [LONGHORN_DRIVER]: 'storageClass.longhorn.title' };
|
||||||
|
|
||||||
|
return csiDrivers.map((provisioner) => {
|
||||||
|
return {
|
||||||
|
label: format[provisioner.name] || provisioner.name,
|
||||||
|
value: provisioner.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
schema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/schemaFor`](STORAGE_CLASS);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
provisionerWatch() {
|
||||||
|
this.$set(this.value, 'parameters', {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getComponent(name) {
|
||||||
|
try {
|
||||||
|
return require(`./provisioners/${ name }`).default;
|
||||||
|
} catch {
|
||||||
|
return require(`./provisioners/custom`).default;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProvisioner(provisioner) {
|
||||||
|
this.$set(this.value, 'provisioner', provisioner);
|
||||||
|
this.$set(this.value, 'allowVolumeExpansion', provisioner === LONGHORN_DRIVER);
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
Object.keys(this.value.parameters).forEach((key) => {
|
||||||
|
if (this.value.parameters[key] === null || this.value.parameters[key] === '') {
|
||||||
|
delete this.value.parameters[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.formatAllowedTopoloties();
|
||||||
|
},
|
||||||
|
|
||||||
|
formatAllowedTopoloties() {
|
||||||
|
const neu = this.allowedTopologies;
|
||||||
|
|
||||||
|
if (!neu || neu.length === 0) {
|
||||||
|
delete this.value.allowedTopologies;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchLabelExpressions = neu.filter(R => !!R.key.trim() && (R.values.length > 0 && !R.values.find(V => !V.trim())));
|
||||||
|
|
||||||
|
if (matchLabelExpressions.length > 0) {
|
||||||
|
this.value.allowedTopologies = [{ matchLabelExpressions }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:subtypes="[]"
|
||||||
|
:validation-passed="true"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:errors="errors"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:namespaced="false"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:register-before-hook="registerBeforeHook"
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
:value="value.provisioner"
|
||||||
|
label="Provisioner"
|
||||||
|
:options="provisioners"
|
||||||
|
:localized-label="true"
|
||||||
|
:mode="modeOverride"
|
||||||
|
:searchable="true"
|
||||||
|
:taggable="true"
|
||||||
|
class="mb-20"
|
||||||
|
@input="updateProvisioner($event)"
|
||||||
|
/>
|
||||||
|
<Tabbed :side-tabs="true">
|
||||||
|
<Tab name="parameters" :label="t('storageClass.parameters.label')" :weight="2">
|
||||||
|
<component
|
||||||
|
:is="getComponent(value.provisioner)"
|
||||||
|
:key="value.provisioner"
|
||||||
|
:value="value"
|
||||||
|
:mode="modeOverride"
|
||||||
|
:real-mode="realMode"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab name="customize" :label="t('storageClass.customize.label')">
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.reclaimPolicy"
|
||||||
|
name="reclaimPolicy"
|
||||||
|
:label="t('storageClass.customize.reclaimPolicy.label')"
|
||||||
|
:mode="modeOverride"
|
||||||
|
:options="reclaimPolicyOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.allowVolumeExpansion"
|
||||||
|
name="allowVolumeExpansion"
|
||||||
|
:label="t('storageClass.customize.allowVolumeExpansion.label')"
|
||||||
|
:mode="modeOverride"
|
||||||
|
:options="allowVolumeExpansionOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.volumeBindingMode"
|
||||||
|
name="volumeBindingMode"
|
||||||
|
:label="t('storageClass.customize.volumeBindingMode.label')"
|
||||||
|
:mode="modeOverride"
|
||||||
|
:options="volumeBindingModeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="allowedTopologies"
|
||||||
|
:label="t('harvester.storage.allowedTopologies.title')"
|
||||||
|
:weight="-1"
|
||||||
|
:tooltip="t('harvester.storage.allowedTopologies.tooltip')"
|
||||||
|
>
|
||||||
|
<ArrayList
|
||||||
|
v-model="allowedTopologies"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:initial-empty-row="true"
|
||||||
|
:show-header="true"
|
||||||
|
:mode="modeOverride"
|
||||||
|
>
|
||||||
|
<template v-slot:column-headers>
|
||||||
|
<div class="box">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-4 key">
|
||||||
|
{{ t('generic.key') }}
|
||||||
|
<span class="required">*</span>
|
||||||
|
</div>
|
||||||
|
<div class="col span-8 value">
|
||||||
|
{{ t('generic.value') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:columns="scope">
|
||||||
|
<div class="row custom-headers">
|
||||||
|
<div class="col span-4 key">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="scope.row.value.key"
|
||||||
|
:required="true"
|
||||||
|
:mode="modeOverride"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-8 value">
|
||||||
|
<Tags
|
||||||
|
v-model="scope.row.value.values"
|
||||||
|
:add-label="t('generic.add')"
|
||||||
|
:mode="modeOverride"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.custom-headers {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { KeyValue },
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<KeyValue
|
||||||
|
v-model="value.parameters"
|
||||||
|
:add-label="t('storageClass.custom.addLabel')"
|
||||||
|
:read-allowed="false"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,249 @@
|
|||||||
|
<script>
|
||||||
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import RadioGroup from '@components/Form/Radio/RadioGroup';
|
||||||
|
|
||||||
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { LONGHORN } from '@shell/config/types';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { uniq } from '@shell/utils/array';
|
||||||
|
|
||||||
|
const DEFAULT_PARAMETERS = [
|
||||||
|
'numberOfReplicas',
|
||||||
|
'staleReplicaTimeout',
|
||||||
|
'diskSelector',
|
||||||
|
'nodeSelector',
|
||||||
|
'migratable',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
KeyValue,
|
||||||
|
LabeledSelect,
|
||||||
|
LabeledInput,
|
||||||
|
RadioGroup,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
realMode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
if (this.realMode === _CREATE) {
|
||||||
|
this.$set(this.value, 'parameters', {
|
||||||
|
numberOfReplicas: '3',
|
||||||
|
staleReplicaTimeout: '30',
|
||||||
|
diskSelector: null,
|
||||||
|
nodeSelector: null,
|
||||||
|
migratable: 'true',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
longhornNodes() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](LONGHORN.NODES);
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeTags() {
|
||||||
|
return (this.longhornNodes || []).reduce((sum, node) => {
|
||||||
|
const tags = node.spec?.tags || [];
|
||||||
|
|
||||||
|
return uniq([...sum, ...tags]);
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
diskTags() {
|
||||||
|
return (this.longhornNodes || []).reduce((sum, node) => {
|
||||||
|
const disks = node.spec?.disks;
|
||||||
|
|
||||||
|
const tagsOfNode = Object.keys(disks).reduce((sum, key) => {
|
||||||
|
const tags = disks[key]?.tags || [];
|
||||||
|
|
||||||
|
return uniq([...sum, ...tags]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return uniq([...sum, ...tagsOfNode]);
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
migratableOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('generic.yes'),
|
||||||
|
value: 'true'
|
||||||
|
}, {
|
||||||
|
label: this.t('generic.no'),
|
||||||
|
value: 'false'
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
parameters: {
|
||||||
|
get() {
|
||||||
|
const parameters = clone(this.value?.parameters) || {};
|
||||||
|
|
||||||
|
DEFAULT_PARAMETERS.map((key) => {
|
||||||
|
delete parameters[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
Object.assign(this.value.parameters, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeSelector: {
|
||||||
|
get() {
|
||||||
|
const nodeSelector = this.value?.parameters?.nodeSelector;
|
||||||
|
|
||||||
|
if ((nodeSelector || '').includes(',')) {
|
||||||
|
return nodeSelector.split(',');
|
||||||
|
} else if (nodeSelector) {
|
||||||
|
return [nodeSelector];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this.value.parameters.nodeSelector = (value || []).join(',');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
diskSelector: {
|
||||||
|
get() {
|
||||||
|
const diskSelector = this.value?.parameters?.diskSelector;
|
||||||
|
|
||||||
|
if ((diskSelector || '').includes(',')) {
|
||||||
|
return diskSelector.split(',');
|
||||||
|
} else if (diskSelector) {
|
||||||
|
return [diskSelector];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this.value.parameters.diskSelector = (value || []).join(',');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
numberOfReplicas: {
|
||||||
|
get() {
|
||||||
|
return this.value?.parameters?.numberOfReplicas;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
if (value >= 1 && value <= 3) {
|
||||||
|
this.value.parameters.numberOfReplicas = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="numberOfReplicas"
|
||||||
|
:label="t('harvester.storage.parameters.numberOfReplicas.label')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
min="1"
|
||||||
|
max="3"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="value.parameters.staleReplicaTimeout"
|
||||||
|
:label="t('harvester.storage.parameters.staleReplicaTimeout.label')"
|
||||||
|
:required="true"
|
||||||
|
:mode="mode"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="nodeSelector"
|
||||||
|
:label="t('harvester.storage.parameters.nodeSelector.label')"
|
||||||
|
:options="nodeTags"
|
||||||
|
:taggable="true"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<template #no-options="{ searching }">
|
||||||
|
<span v-if="!searching" class="text-muted">
|
||||||
|
{{ t('harvester.storage.parameters.nodeSelector.no-options', null, true) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</LabeledSelect>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="diskSelector"
|
||||||
|
:label="t('harvester.storage.parameters.diskSelector.label')"
|
||||||
|
:options="diskTags"
|
||||||
|
:taggable="true"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<template #no-options="{ searching }">
|
||||||
|
<span v-if="!searching" class="text-muted">
|
||||||
|
{{ t('harvester.storage.parameters.diskSelector.no-options', null, true) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</LabeledSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="value.parameters.migratable"
|
||||||
|
name="layer3NetworkMode"
|
||||||
|
:label="t('harvester.storage.parameters.migratable.label')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="migratableOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<KeyValue
|
||||||
|
v-model="parameters"
|
||||||
|
:add-label="t('storageClass.longhorn.addLabel')"
|
||||||
|
:read-allowed="false"
|
||||||
|
:mode="mode"
|
||||||
|
class="mt-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.labeled-input.compact-input {
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
299
pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue
Normal file
299
pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
<script>
|
||||||
|
import Footer from '@shell/components/form/Footer';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import Checkbox from '@components/Form/Checkbox/Checkbox';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { sortBy } from '@shell/utils/sort';
|
||||||
|
import { clone } from '@shell/utils/object';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { BACKUP_TYPE } from '../config/types';
|
||||||
|
|
||||||
|
const createObject = {
|
||||||
|
apiVersion: 'harvesterhci.io/v1beta1',
|
||||||
|
kind: 'VirtualMachineRestore',
|
||||||
|
metadata: { name: '', namespace: '' },
|
||||||
|
type: HCI.RESTORE,
|
||||||
|
spec: {
|
||||||
|
target: {
|
||||||
|
apiGroup: 'kubevirt.io',
|
||||||
|
kind: 'VirtualMachine',
|
||||||
|
name: ''
|
||||||
|
},
|
||||||
|
virtualMachineBackupName: '',
|
||||||
|
newVM: true,
|
||||||
|
deletionPolicy: 'delete'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CreateRestore',
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
Footer,
|
||||||
|
RadioGroup,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await allHash({
|
||||||
|
backups: this.$store.dispatch('harvester/findAll', { type: HCI.BACKUP }),
|
||||||
|
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const restoreMode = this.$route.query?.restoreMode;
|
||||||
|
const backupName = this.$route.query?.resourceName;
|
||||||
|
|
||||||
|
const restoreResource = clone(createObject);
|
||||||
|
|
||||||
|
const restoreNewVm = restoreMode === 'new' || restoreMode === undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
backupName,
|
||||||
|
restoreNewVm,
|
||||||
|
restoreResource,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
deletionPolicy: 'delete',
|
||||||
|
namespace: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
backupOption() {
|
||||||
|
const choices = this.$store.getters['harvester/all'](HCI.BACKUP);
|
||||||
|
|
||||||
|
return choices.filter( (T) => {
|
||||||
|
const hasVM = this.restoreNewVm || T.attachVmExisting;
|
||||||
|
|
||||||
|
return hasVM && T?.status?.readyToUse && T.spec?.type !== BACKUP_TYPE.SNAPSHOT;
|
||||||
|
}).map( (T) => {
|
||||||
|
return {
|
||||||
|
label: T.metadata.name,
|
||||||
|
value: T.metadata.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deletionPolicyOption() {
|
||||||
|
return [{
|
||||||
|
value: 'delete',
|
||||||
|
label: 'Delete'
|
||||||
|
}, {
|
||||||
|
value: 'retain',
|
||||||
|
label: 'Retain'
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
|
||||||
|
currentBackupResource() {
|
||||||
|
const name = this.backupName;
|
||||||
|
|
||||||
|
const backupList = this.$store.getters['harvester/all'](HCI.BACKUP);
|
||||||
|
|
||||||
|
return backupList.find( O => O.name === name);
|
||||||
|
},
|
||||||
|
|
||||||
|
disableExisting() {
|
||||||
|
return !this.currentBackupResource?.attachVmExisting;
|
||||||
|
},
|
||||||
|
|
||||||
|
backupNamespace() {
|
||||||
|
const backupList = this.$store.getters['harvester/all'](HCI.BACKUP);
|
||||||
|
|
||||||
|
return backupList.find( B => B.metadata.name === this.backupName)?.metadata?.namespace;
|
||||||
|
},
|
||||||
|
|
||||||
|
namespaces() {
|
||||||
|
const allNamespaces = this.$store.getters['allNamespaces'];
|
||||||
|
|
||||||
|
const out = sortBy(
|
||||||
|
allNamespaces.map((obj) => {
|
||||||
|
return {
|
||||||
|
label: obj.nameDisplay,
|
||||||
|
value: obj.id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
'label'
|
||||||
|
);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
backupName: {
|
||||||
|
handler(neu) {
|
||||||
|
if (this.currentBackupResource) {
|
||||||
|
if (!this.restoreNewVm) {
|
||||||
|
this.name = this?.currentBackupResource?.attachVM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restoreResource.spec.virtualMachineBackupName = neu;
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
|
||||||
|
restoreNewVm(neu) {
|
||||||
|
if (neu) {
|
||||||
|
this.name = '';
|
||||||
|
} else {
|
||||||
|
this.name = this?.currentBackupResource?.attachVM;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
backupNamespace: {
|
||||||
|
handler(neu) {
|
||||||
|
this.namespace = neu;
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveRestore(buttonCb) {
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
const proxyResource = await this.$store.dispatch('harvester/create', this.restoreResource);
|
||||||
|
|
||||||
|
proxyResource.metadata.namespace = this.namespace;
|
||||||
|
proxyResource.spec.virtualMachineBackupNamespace = this.backupNamespace;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await proxyResource.save();
|
||||||
|
buttonCb(true);
|
||||||
|
|
||||||
|
this.$router.push({
|
||||||
|
name: this.doneRoute,
|
||||||
|
params: { resource: HCI.VM }
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err) || err;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.restoreResource.metadata.generateName = `restore-${ this.backupName }-`;
|
||||||
|
if (this.name) {
|
||||||
|
this.restoreResource.spec.target.name = this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.restoreNewVm) {
|
||||||
|
delete this.restoreResource.spec.deletionPolicy;
|
||||||
|
this.restoreResource.spec.newVM = true;
|
||||||
|
} else {
|
||||||
|
this.restoreResource.spec.deletionPolicy = this.deletionPolicy;
|
||||||
|
delete this.restoreResource.spec.newVM;
|
||||||
|
delete this.restoreResource.spec.keepMacAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentTitle() {
|
||||||
|
return 'restoreVM';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="restore">
|
||||||
|
<div class="content">
|
||||||
|
<div class="mb-20">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="restoreNewVm"
|
||||||
|
name="model"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('harvester.backup.restore.createNew'), t('harvester.backup.restore.replaceExisting')]"
|
||||||
|
:disabled="disableExisting"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="namespace"
|
||||||
|
:disabled="!restoreNewVm"
|
||||||
|
:label="t('nameNsDescription.namespace.label')"
|
||||||
|
:options="namespaces"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="name"
|
||||||
|
:disabled="!restoreNewVm"
|
||||||
|
:label="t('harvester.backup.restore.virtualMachineName')"
|
||||||
|
:placeholder="t('nameNsDescription.name.placeholder')"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="backupName"
|
||||||
|
class="mb-20"
|
||||||
|
:label="t('harvester.backup.restore.backup')"
|
||||||
|
:options="backupOption"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
v-if="restoreNewVm"
|
||||||
|
v-model="restoreResource.spec.keepMacAddress"
|
||||||
|
type="checkbox"
|
||||||
|
:label="t('harvester.backup.restore.keepMacAddress')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="!restoreNewVm"
|
||||||
|
v-model="deletionPolicy"
|
||||||
|
:label="t('harvester.backup.restore.deletePreviousVolumes')"
|
||||||
|
:options="deletionPolicyOption"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="done" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#restore {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
::v-deep .radio-group {
|
||||||
|
display: flex;
|
||||||
|
.radio-container {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
border-top: var(--header-border-size) solid var(--header-border);
|
||||||
|
|
||||||
|
// Overrides outlet padding
|
||||||
|
margin-left: -$space-m;
|
||||||
|
margin-right: -$space-m;
|
||||||
|
margin-bottom: -$space-m;
|
||||||
|
padding: $space-s $space-m;
|
||||||
|
|
||||||
|
::v-deep .spacer-small {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
446
pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue
Normal file
446
pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import Select from '@shell/components/form/Select';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { OS } from '../mixins/harvester-vm';
|
||||||
|
import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { STORAGE_CLASS } from '@shell/config/types';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
|
const DOWNLOAD = 'download';
|
||||||
|
const UPLOAD = 'upload';
|
||||||
|
const rawORqcow2 = 'raw_qcow2';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditImage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Tab,
|
||||||
|
Tabbed,
|
||||||
|
KeyValue,
|
||||||
|
Select,
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
NameNsDescription,
|
||||||
|
RadioGroup,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
images: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IMAGE }),
|
||||||
|
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find(s => s.isDefault);
|
||||||
|
|
||||||
|
this.$set(this, 'storageClassName', this.storageClassName || defaultStorage?.metadata?.name || 'longhorn');
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
if ( !this.value.spec ) {
|
||||||
|
this.$set(this.value, 'spec', { sourceType: DOWNLOAD });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.value.metadata.name) {
|
||||||
|
this.value.metadata.generateName = 'image-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: this.value.spec.url,
|
||||||
|
files: [],
|
||||||
|
resource: '',
|
||||||
|
headers: {},
|
||||||
|
fileUrl: '',
|
||||||
|
file: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
uploadFileName() {
|
||||||
|
return this.file?.name || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
imageName() {
|
||||||
|
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
||||||
|
},
|
||||||
|
|
||||||
|
isCreateEdit() {
|
||||||
|
return this.isCreate || this.isEdit;
|
||||||
|
},
|
||||||
|
|
||||||
|
showEditAsYaml() {
|
||||||
|
return this.value.spec.sourceType === DOWNLOAD;
|
||||||
|
},
|
||||||
|
|
||||||
|
storageClassOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
|
const out = storages.filter(s => !s.parameters?.backingImage).map((s) => {
|
||||||
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: s.name,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
storageClassName: {
|
||||||
|
get() {
|
||||||
|
return this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||||
|
},
|
||||||
|
|
||||||
|
set(nue) {
|
||||||
|
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'value.spec.url'(neu) {
|
||||||
|
const url = neu.trim();
|
||||||
|
|
||||||
|
this.setImageLabels(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
'value.spec.sourceType'() {
|
||||||
|
this.$set(this, 'file', null);
|
||||||
|
this.url = '';
|
||||||
|
|
||||||
|
if (this.$refs?.file?.value) {
|
||||||
|
this.$refs.file.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveImage(buttonCb) {
|
||||||
|
this.value.spec.displayName = (this.value.spec.displayName || '').trim();
|
||||||
|
|
||||||
|
if (this.value.spec.sourceType === UPLOAD && this.isCreate) {
|
||||||
|
try {
|
||||||
|
this.value.spec.url = '';
|
||||||
|
|
||||||
|
const file = this.file;
|
||||||
|
|
||||||
|
this.value.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = file?.name;
|
||||||
|
|
||||||
|
const res = await this.value.save();
|
||||||
|
|
||||||
|
res.uploadImage(file);
|
||||||
|
|
||||||
|
buttonCb(true);
|
||||||
|
this.done();
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = exceptionToErrorsArray(e);
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.save(buttonCb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setImageLabels(str) {
|
||||||
|
const suffixName = str?.split('/')?.pop() || str;
|
||||||
|
|
||||||
|
const fileSuffix = suffixName?.split('.')?.pop()?.toLowerCase();
|
||||||
|
|
||||||
|
if (VM_IMAGE_FILE_FORMAT.includes(fileSuffix)) {
|
||||||
|
const labelValue = fileSuffix === 'iso' ? fileSuffix : rawORqcow2;
|
||||||
|
|
||||||
|
this.addLabel(HCI_ANNOTATIONS.IMAGE_SUFFIX, labelValue);
|
||||||
|
|
||||||
|
if (!this.value.spec.displayName) {
|
||||||
|
this.$refs.nd.changeNameAndNamespace({
|
||||||
|
text: suffixName,
|
||||||
|
selected: this.value.metadata.namespace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const os = this.getOSType(str);
|
||||||
|
|
||||||
|
if (os) {
|
||||||
|
this.addLabel(HCI_ANNOTATIONS.OS_TYPE, os.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addLabel(labelKey, value) {
|
||||||
|
const rows = this.$refs.labels.rows;
|
||||||
|
|
||||||
|
rows.map((label, idx) => {
|
||||||
|
if (label.key === labelKey) {
|
||||||
|
this.$refs.labels.remove(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$refs.labels.add(labelKey, value);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFileUpload() {
|
||||||
|
const file = this.$refs.file.files[0];
|
||||||
|
|
||||||
|
this.file = file;
|
||||||
|
|
||||||
|
this.setImageLabels(file?.name);
|
||||||
|
|
||||||
|
if (!this.value.spec.displayName) {
|
||||||
|
this.$refs.nd.changeNameAndNamespace({
|
||||||
|
text: file?.name,
|
||||||
|
selected: this.value.metadata.namespace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setImageLabels();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectFile() {
|
||||||
|
// Clear the value so the user can reselect the same file again
|
||||||
|
this.$refs.file.value = null;
|
||||||
|
this.$refs.file.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
internalAnnotations(option) {
|
||||||
|
const optionKeys = [HCI_ANNOTATIONS.OS_TYPE, HCI_ANNOTATIONS.IMAGE_SUFFIX];
|
||||||
|
|
||||||
|
return optionKeys.find(O => O === option.key);
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateOptions(keyName) {
|
||||||
|
if (keyName === HCI_ANNOTATIONS.OS_TYPE) {
|
||||||
|
return OS;
|
||||||
|
} else if (keyName === HCI_ANNOTATIONS.IMAGE_SUFFIX) {
|
||||||
|
return [{
|
||||||
|
label: 'ISO',
|
||||||
|
value: 'iso'
|
||||||
|
}, {
|
||||||
|
label: 'raw/qcow2',
|
||||||
|
value: rawORqcow2
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
focusKey() {
|
||||||
|
this.$refs.key.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
getOSType(str) {
|
||||||
|
if (!str) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OS.find( (os) => {
|
||||||
|
if (os.match) {
|
||||||
|
return os.match.find(matchValue => str.toLowerCase().includes(matchValue)) ? os.value : false;
|
||||||
|
} else {
|
||||||
|
return str.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
:can-yaml="showEditAsYaml ? true : false"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
@finish="saveImage"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
ref="nd"
|
||||||
|
v-model="value"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('generic.name')"
|
||||||
|
name-key="spec.displayName"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||||
|
<Tab
|
||||||
|
name="basic"
|
||||||
|
:label="t('harvester.image.tabs.basics')"
|
||||||
|
:weight="99"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
v-if="isCreate"
|
||||||
|
v-model="value.spec.sourceType"
|
||||||
|
name="model"
|
||||||
|
:options="[
|
||||||
|
'download',
|
||||||
|
'upload',
|
||||||
|
]"
|
||||||
|
:labels="[
|
||||||
|
t('harvester.image.sourceType.download'),
|
||||||
|
t('harvester.image.sourceType.upload'),
|
||||||
|
]"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<div class="row mb-20 mt-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput
|
||||||
|
v-if="!isCreate"
|
||||||
|
v-model="value.spec.sourceType"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
:disabled="isEdit"
|
||||||
|
label-key="harvester.image.source"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="value.spec.sourceType === 'download'"
|
||||||
|
v-model="value.spec.url"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="isEdit"
|
||||||
|
class="mb-20 labeled-input--tooltip"
|
||||||
|
required
|
||||||
|
label-key="harvester.image.url"
|
||||||
|
:tooltip="t('harvester.image.urlTip', {}, true)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<LabeledInput
|
||||||
|
v-if="isView"
|
||||||
|
v-model="imageName"
|
||||||
|
:mode="mode"
|
||||||
|
class="mt-20"
|
||||||
|
label-key="harvester.image.fileName"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="isCreate"
|
||||||
|
type="button"
|
||||||
|
class="btn role-primary"
|
||||||
|
@click="selectFile"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ t('harvester.image.uploadFile') }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-show="false"
|
||||||
|
id="file"
|
||||||
|
ref="file"
|
||||||
|
type="file"
|
||||||
|
accept=".qcow, .qcow2, .raw, .img, .iso"
|
||||||
|
@change="handleFileUpload()"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="uploadFileName"
|
||||||
|
class="fileName mt-5"
|
||||||
|
>
|
||||||
|
<span class="icon icon-file" />
|
||||||
|
{{ uploadFileName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="value.spec.sourceType === 'download'"
|
||||||
|
v-model="value.spec.checksum"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="isEdit"
|
||||||
|
label-key="harvester.image.checksum"
|
||||||
|
:tooltip="t('harvester.image.checksumTip')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="storage"
|
||||||
|
:label="t('harvester.storage.label')"
|
||||||
|
:weight="89"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model="storageClassName"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
:label="t('harvester.storage.storageClass.label')"
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="isEdit"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab name="labels" :label="t('labels.labels.title')" :weight="2" class="bordered-table">
|
||||||
|
<KeyValue
|
||||||
|
key="labels"
|
||||||
|
ref="labels"
|
||||||
|
:value="value.labels"
|
||||||
|
:add-label="t('labels.addLabel')"
|
||||||
|
:mode="mode"
|
||||||
|
:pad-left="false"
|
||||||
|
:read-allowed="false"
|
||||||
|
@focusKey="focusKey"
|
||||||
|
@input="value.setLabels($event)"
|
||||||
|
>
|
||||||
|
<template #value="{row, keyName, valueName, queueUpdate}">
|
||||||
|
<Select
|
||||||
|
v-if="internalAnnotations(row)"
|
||||||
|
v-model="row[valueName]"
|
||||||
|
:mode="mode"
|
||||||
|
:searchable="true"
|
||||||
|
:clearable="false"
|
||||||
|
:options="calculateOptions(row[keyName])"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="row[valueName]"
|
||||||
|
:disabled="isView"
|
||||||
|
:type="'text'"
|
||||||
|
:placeholder="t('keyValue.valuePlaceholder')"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</KeyValue>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.kv-item.value > .unlabeled-select {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user