mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Add pkg/harvester components + shell portings - 1
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { HCI } from '../types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { HCI } from '../types';
|
||||
import UpgradeInfo from './UpgradeInfo';
|
||||
export default {
|
||||
name: 'HarvesterUpgrade',
|
||||
@@ -34,7 +34,8 @@ export default {
|
||||
selectMode: true,
|
||||
version: '',
|
||||
enableLogging: true,
|
||||
readyReleaseNote: false
|
||||
readyReleaseNote: false,
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -44,7 +45,7 @@ export default {
|
||||
versionOptions() {
|
||||
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
|
||||
|
||||
return versions.map(V => V.metadata.name);
|
||||
return versions.map((V) => V.metadata.name);
|
||||
},
|
||||
|
||||
currentVersion() {
|
||||
@@ -68,7 +69,7 @@ export default {
|
||||
let upgradeMessage = [];
|
||||
const list = neu || [];
|
||||
|
||||
const currentResource = list.find( O => !!O.isLatestUpgrade);
|
||||
const currentResource = list.find( (O) => !!O.isLatestUpgrade);
|
||||
|
||||
upgradeMessage = currentResource ? currentResource.upgradeMessage : [];
|
||||
|
||||
@@ -111,12 +112,12 @@ export default {
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.$refs.deleteTip.hide();
|
||||
this.isOpen = false;
|
||||
this.errors = '';
|
||||
},
|
||||
|
||||
open() {
|
||||
this.$refs.deleteTip.open();
|
||||
this.isOpen = true;
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -131,12 +132,21 @@ export default {
|
||||
:cluster="currentCluster.nameDisplay"
|
||||
/>
|
||||
</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" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<ModalWithCard ref="deleteTip" name="deleteTip" :width="850">
|
||||
<ModalWithCard
|
||||
v-if="isOpen"
|
||||
name="deleteTip"
|
||||
:width="850"
|
||||
>
|
||||
<template #title>
|
||||
<t k="harvester.upgradePage.upgradeApp" />
|
||||
</template>
|
||||
@@ -158,17 +168,36 @@ export default {
|
||||
:clearable="true"
|
||||
/>
|
||||
|
||||
<div v-if="canEnableLogging" class="mb-5">
|
||||
<Checkbox v-model="enableLogging" class="check" type="checkbox" :label="t('harvester.upgradePage.enableLogging')" />
|
||||
<div
|
||||
v-if="canEnableLogging"
|
||||
class="mb-5"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="enableLogging"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.upgradePage.enableLogging')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<Banner v-if="errors.length" color="warning">
|
||||
<Banner
|
||||
v-if="errors.length"
|
||||
color="warning"
|
||||
>
|
||||
{{ errors }}
|
||||
</Banner>
|
||||
</div>
|
||||
@@ -176,10 +205,17 @@ export default {
|
||||
|
||||
<template #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" />
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script>
|
||||
import { NODE } from '@shell/config/types';
|
||||
import { HCI } from '../types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
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 BadgeStateFormatter from '@shell/components/formatter/BadgeStateFormatter';
|
||||
import { PRODUCT_NAME as HARVESTER } from '../config/harvester';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterUpgradeHeader',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user