mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-06 22:26:05 +08:00
Compare commits
15
Commits
v1.6.0-rc3
...
v1.6.0-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82ba9d9f5 | ||
|
|
f7fc001abd | ||
|
|
d1c676ee2c | ||
|
|
c562dd73ba | ||
|
|
1cd5e5ca7d | ||
|
|
1aaa8f71c4 | ||
|
|
003a09d0a9 | ||
|
|
f08e632aad | ||
|
|
df1d7001bc | ||
|
|
41f78c83a7 | ||
|
|
f1a53311ce | ||
|
|
e0c6e905a9 | ||
|
|
1852e434f1 | ||
|
|
77c085d3a7 | ||
|
|
9dbe7d9015 |
@@ -4,4 +4,3 @@ numberOfReviewers: 0
|
|||||||
reviewers:
|
reviewers:
|
||||||
- a110605
|
- a110605
|
||||||
- houhoucoop
|
- houhoucoop
|
||||||
- NickChungSUSE
|
|
||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.6.0-rc3",
|
"version": "1.6.0-rc6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
"@babel/plugin-transform-class-static-block": "7.26.0",
|
||||||
"@rancher/shell": "3.0.5-rc.6",
|
"@rancher/shell": "3.0.5-rc.7",
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
|
|||||||
@@ -98,8 +98,12 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||||
|
|
||||||
|
// remove <a> tags from description
|
||||||
|
if (description.includes('<a')) {
|
||||||
|
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
||||||
|
}
|
||||||
// filter by description
|
// filter by description
|
||||||
if (description.includes(searchQuery)) {
|
if (description.includes(searchQuery)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
if (this.availableNamespaces.length === 0) return [];
|
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
this.namespaces.length === this.filteredNamespaces.length &&
|
this.namespaces.length === this.filteredNamespaces.length &&
|
||||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ const FEATURE_FLAGS = {
|
|||||||
'vmNetworkMigration',
|
'vmNetworkMigration',
|
||||||
'kubeovnVpcSubnet',
|
'kubeovnVpcSubnet',
|
||||||
'rancherClusterSetting',
|
'rancherClusterSetting',
|
||||||
'cpuMemoryHotplug'
|
'cpuMemoryHotplug',
|
||||||
|
'cdiSettings',
|
||||||
|
'vmCloneRunStrategy',
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -72,4 +72,8 @@ export const HCI = {
|
|||||||
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||||
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
||||||
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
||||||
|
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
|
||||||
|
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||||
|
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||||
|
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -113,7 +113,10 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.RANCHER_CLUSTER]: {
|
[HCI_SETTING.RANCHER_CLUSTER]: {
|
||||||
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||||
|
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
|
|||||||
@@ -47,20 +47,26 @@ export default {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME,
|
NAME,
|
||||||
{
|
{
|
||||||
name: 'vmCPU',
|
name: 'CPU',
|
||||||
labelKey: 'tableHeaders.cpu',
|
label: 'CPU',
|
||||||
search: false,
|
sort: ['displayCPU'],
|
||||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
value: 'displayCPU',
|
||||||
value: 'spec.template.spec.domain.cpu.cores',
|
align: 'center',
|
||||||
width: 120
|
dashIfEmpty: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vmRAM',
|
name: 'Memory',
|
||||||
labelKey: 'glance.memory',
|
value: 'displayMemory',
|
||||||
search: false,
|
sort: ['memorySort'],
|
||||||
sort: ['memorySort'],
|
align: 'center',
|
||||||
value: 'spec.template.spec.domain.resources.limits.memory',
|
labelKey: 'tableHeaders.memory',
|
||||||
width: 120
|
formatter: 'Si',
|
||||||
|
formatterOpts: {
|
||||||
|
opts: {
|
||||||
|
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
||||||
|
},
|
||||||
|
needParseSi: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ip',
|
name: 'ip',
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CloneVMModal',
|
name: 'CloneVMModal',
|
||||||
@@ -14,7 +15,7 @@ export default {
|
|||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -26,9 +27,11 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
cloneData: true,
|
cloneData: true,
|
||||||
errors: []
|
errors: [],
|
||||||
|
runStrategy: runStrategyOptions[1],
|
||||||
|
runStrategyOptions
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -38,6 +41,9 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
vmCloneRunStrategyEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -58,7 +64,8 @@ export default {
|
|||||||
|
|
||||||
// deep clone
|
// deep clone
|
||||||
try {
|
try {
|
||||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
||||||
|
const res = await this.actionResource.doAction('clone', payload, {}, false);
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch('growl/success', {
|
this.$store.dispatch('growl/success', {
|
||||||
@@ -106,6 +113,13 @@ export default {
|
|||||||
:label="t('harvester.modal.cloneVM.name')"
|
:label="t('harvester.modal.cloneVM.name')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="vmCloneRunStrategyEnabled"
|
||||||
|
v-model:value="runStrategy"
|
||||||
|
label-key="harvester.virtualMachine.runStrategy"
|
||||||
|
:options="runStrategyOptions"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-for="(err, i) in errors"
|
v-for="(err, i) in errors"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CloneVMModal',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: '',
|
|
||||||
cloneData: true,
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources[0];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.name = '';
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async create(buttonCb) {
|
|
||||||
// shadow clone
|
|
||||||
if (!this.cloneData) {
|
|
||||||
this.resources[0].goToClone();
|
|
||||||
buttonCb(false);
|
|
||||||
this.close();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// deep clone
|
|
||||||
try {
|
|
||||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
|
||||||
this.$store.dispatch('growl/success', {
|
|
||||||
title: this.t('harvester.notification.title.succeed'),
|
|
||||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
|
||||||
}, { root: true });
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
buttonCb(true);
|
|
||||||
} else {
|
|
||||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
|
||||||
|
|
||||||
this['errors'] = error;
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
const message = exceptionToErrorsArray(error);
|
|
||||||
|
|
||||||
this['errors'] = message;
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.cloneVM.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="cloneData"
|
|
||||||
class="mb-10"
|
|
||||||
label-key="harvester.modal.cloneVM.type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-show="cloneData"
|
|
||||||
v-model:value="name"
|
|
||||||
class="mb-20"
|
|
||||||
:label="t('harvester.modal.cloneVM.name')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
:label="err"
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="actions">
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="create"
|
|
||||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
|
||||||
:disabled="cloneData && !name"
|
|
||||||
@click="create"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
<script>
|
||||||
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CDISettings',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
provisioner: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ArrayList,
|
||||||
|
Checkbox,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:cdiSettings'],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const hash = {
|
||||||
|
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
|
||||||
|
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
|
||||||
|
if (this.mode === _CREATE ) {
|
||||||
|
this.setDefaultVolumeSnapshotClass();
|
||||||
|
} else {
|
||||||
|
this.initCDISettingsFromAnnotations();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cdiSettings: {
|
||||||
|
volumeModeAccessModes: [],
|
||||||
|
volumeSnapshotClass: null,
|
||||||
|
cloneStrategy: null,
|
||||||
|
filesystemOverhead: null,
|
||||||
|
},
|
||||||
|
defaultAddValue: { volumeMode: null, accessModes: [] },
|
||||||
|
noneOption: { label: 'None', value: '' },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
inStore() {
|
||||||
|
return this.$store.getters['currentProduct'].inStore;
|
||||||
|
},
|
||||||
|
|
||||||
|
allVolumeModeOptions() {
|
||||||
|
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedVolumeModes() {
|
||||||
|
return this.cdiSettings.volumeModeAccessModes
|
||||||
|
.map((item) => item.volumeMode)
|
||||||
|
.filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeModeOptions() {
|
||||||
|
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
|
||||||
|
},
|
||||||
|
|
||||||
|
allVolumeModesSelected() {
|
||||||
|
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
accessModeOptions() {
|
||||||
|
return [
|
||||||
|
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
||||||
|
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
||||||
|
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
||||||
|
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
cloneStrategyOptions() {
|
||||||
|
return [
|
||||||
|
this.noneOption,
|
||||||
|
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
|
||||||
|
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
|
||||||
|
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeSnapshotClassOptions() {
|
||||||
|
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||||
|
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
|
||||||
|
|
||||||
|
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
|
||||||
|
},
|
||||||
|
|
||||||
|
isFilesystemOverheadValid() {
|
||||||
|
const val = this.cdiSettings.filesystemOverhead;
|
||||||
|
|
||||||
|
if (val === null || val === '') return true;
|
||||||
|
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
|
||||||
|
|
||||||
|
return regex.test(val);
|
||||||
|
},
|
||||||
|
|
||||||
|
isCustomClass() {
|
||||||
|
return this.mode === _CREATE || this.mode === _EDIT;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
provisioner() {
|
||||||
|
this.resetCdiSettings();
|
||||||
|
this.$nextTick(this.setDefaultVolumeSnapshotClass);
|
||||||
|
},
|
||||||
|
|
||||||
|
cdiSettings: {
|
||||||
|
handler(val) {
|
||||||
|
this.$emit('update:cdiSettings', val);
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setDefaultVolumeSnapshotClass() {
|
||||||
|
try {
|
||||||
|
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
|
||||||
|
const config = JSON.parse(setting?.value || setting?.default || '{}');
|
||||||
|
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
|
||||||
|
|
||||||
|
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||||
|
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
|
||||||
|
|
||||||
|
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
|
||||||
|
this.cdiSettings.volumeSnapshotClass = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initCDISettingsFromAnnotations() {
|
||||||
|
const annotations = this.value?.metadata?.annotations || {};
|
||||||
|
|
||||||
|
let volumeModeAccessModes = [];
|
||||||
|
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||||
|
|
||||||
|
if (rawVolumeMode) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawVolumeMode);
|
||||||
|
|
||||||
|
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
|
||||||
|
volumeMode,
|
||||||
|
accessModes: Array.isArray(accessModes) ? accessModes : [],
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (volumeModeAccessModes.length) {
|
||||||
|
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
|
||||||
|
}
|
||||||
|
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
|
||||||
|
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||||
|
}
|
||||||
|
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
|
||||||
|
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||||
|
}
|
||||||
|
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
|
||||||
|
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetCdiSettings() {
|
||||||
|
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
|
||||||
|
this.cdiSettings.volumeSnapshotClass = null;
|
||||||
|
this.cdiSettings.cloneStrategy = null;
|
||||||
|
this.cdiSettings.filesystemOverhead = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
validateFilesystemOverhead(e) {
|
||||||
|
const val = e.target.value;
|
||||||
|
|
||||||
|
this.cdiSettings.filesystemOverhead = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="cdiSettings.volumeModeAccessModes"
|
||||||
|
:initial-empty-row="true"
|
||||||
|
:show-header="true"
|
||||||
|
:mode="mode"
|
||||||
|
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
|
||||||
|
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
|
||||||
|
:add-disabled="allVolumeModesSelected"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="column-headers">
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
:class="{ custom: isCustomClass }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="col span-3 value text-label mb-10"
|
||||||
|
for="volumeMode"
|
||||||
|
>
|
||||||
|
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="col span-9 value text-label mb-10"
|
||||||
|
for="accessModes"
|
||||||
|
>
|
||||||
|
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #columns="{ row }">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledSelect
|
||||||
|
id="volumeMode"
|
||||||
|
v-model:value="row.value.volumeMode"
|
||||||
|
:mode="mode"
|
||||||
|
:options="volumeModeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="accessModes"
|
||||||
|
class="col span-9"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
v-for="opt in accessModeOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
v-model:value="row.value.accessModes"
|
||||||
|
:value-when-true="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
type="checkbox"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="cdiSettings.volumeSnapshotClass"
|
||||||
|
class="select mt-20 mb-20"
|
||||||
|
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
|
||||||
|
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="volumeSnapshotClassOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="cdiSettings.cloneStrategy"
|
||||||
|
class="select mb-20"
|
||||||
|
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
|
||||||
|
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="cloneStrategyOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="cdiSettings.filesystemOverhead"
|
||||||
|
class="select mb-20"
|
||||||
|
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
|
||||||
|
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.001"
|
||||||
|
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
|
||||||
|
:status="isFilesystemOverheadValid ? null : 'error'"
|
||||||
|
@input="validateFilesystemOverhead"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.column-headers .row.custom {
|
||||||
|
max-width: calc(100% - 75px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import CruResource from '@shell/components/CruResource';
|
import CruResource from '@shell/components/CruResource';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
@@ -6,11 +7,9 @@ import ArrayList from '@shell/components/form/ArrayList';
|
|||||||
import Tab from '@shell/components/Tabbed/Tab';
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
|
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +21,7 @@ import { CSI_DRIVER } from '../../types';
|
|||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import CDISettings from './CDISettings';
|
||||||
|
|
||||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
Tabbed,
|
Tabbed,
|
||||||
Loading,
|
Loading,
|
||||||
Tags,
|
Tags,
|
||||||
|
CDISettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -117,6 +118,7 @@ export default {
|
|||||||
key: '',
|
key: '',
|
||||||
values: [],
|
values: [],
|
||||||
},
|
},
|
||||||
|
cdiSettings: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -124,10 +126,10 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const hash = {
|
const hash = {
|
||||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
|
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.value.longhornV2LVMSupport) {
|
if (this.value.longhornV2LVMSupport) {
|
||||||
@@ -197,6 +199,14 @@ export default {
|
|||||||
|
|
||||||
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isCDISettingsFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldShowCDISettingsTab() {
|
||||||
|
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -249,6 +259,10 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.formatAllowedTopoloties();
|
this.formatAllowedTopoloties();
|
||||||
|
|
||||||
|
if (this.shouldShowCDISettingsTab) {
|
||||||
|
this.formatCDISettings();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAllowedTopoloties() {
|
formatAllowedTopoloties() {
|
||||||
@@ -270,6 +284,42 @@ export default {
|
|||||||
if (matchLabelExpressions.length > 0) {
|
if (matchLabelExpressions.length > 0) {
|
||||||
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
formatCDISettings() {
|
||||||
|
const annotations = this.value.metadata.annotations || {};
|
||||||
|
const volumeModeAccessModes = {};
|
||||||
|
|
||||||
|
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
|
||||||
|
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
|
||||||
|
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(volumeModeAccessModes).length > 0) {
|
||||||
|
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cdiSettings.volumeSnapshotClass) {
|
||||||
|
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cdiSettings.cloneStrategy) {
|
||||||
|
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cdiSettings.filesystemOverhead) {
|
||||||
|
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.metadata.annotations = annotations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -404,6 +454,19 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ArrayList>
|
</ArrayList>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="shouldShowCDISettingsTab"
|
||||||
|
name="cdiSettings"
|
||||||
|
:label="t('harvester.storage.cdiSettings.label')"
|
||||||
|
:weight="-2"
|
||||||
|
>
|
||||||
|
<CDISettings
|
||||||
|
v-model:cdi-settings="cdiSettings"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:provisioner="value.provisioner"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1080,8 +1080,6 @@ harvester:
|
|||||||
placeholder: e.g. 169.254.0.1/16
|
placeholder: e.g. 169.254.0.1/16
|
||||||
remoteVpc:
|
remoteVpc:
|
||||||
label: Remote VPC
|
label: Remote VPC
|
||||||
|
|
||||||
|
|
||||||
network:
|
network:
|
||||||
label: Virtual Machine Networks
|
label: Virtual Machine Networks
|
||||||
tabs:
|
tabs:
|
||||||
@@ -1388,6 +1386,28 @@ harvester:
|
|||||||
label: Internal Storage Class
|
label: Internal Storage Class
|
||||||
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
|
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
|
||||||
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
|
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
|
||||||
|
cdiSettings:
|
||||||
|
label: CDI Settings
|
||||||
|
volumeModeAccessModes:
|
||||||
|
label: Volume Mode / Access Modes
|
||||||
|
tooltip: Specifies the default volume mode and access modes.
|
||||||
|
add: Add
|
||||||
|
volumeMode: Volume Mode
|
||||||
|
accessModes:
|
||||||
|
label: Access Modes
|
||||||
|
volumeSnapshotClass:
|
||||||
|
label: Volume Snapshot Class
|
||||||
|
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
|
||||||
|
cloneStrategy:
|
||||||
|
label: Clone Strategy
|
||||||
|
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
|
||||||
|
copy: Copy
|
||||||
|
snapshot: Snapshot
|
||||||
|
csiClone: CSI Clone
|
||||||
|
fileSystemOverhead:
|
||||||
|
label: File System Overhead
|
||||||
|
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
|
||||||
|
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
|
||||||
|
|
||||||
vlanConfig:
|
vlanConfig:
|
||||||
title: Network Configuration
|
title: Network Configuration
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export default class HciVmBackup extends HarvesterResource {
|
|||||||
return detailLocation;
|
return detailLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get disableResourceDetailDrawerConfigTab() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
get doneOverride() {
|
get doneOverride() {
|
||||||
const route = this.currentRoute();
|
const route = this.currentRoute();
|
||||||
const detailLocation = clone(this._detailLocation);
|
const detailLocation = clone(this._detailLocation);
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ const PAUSED_VM_MODAL_MESSAGE =
|
|||||||
|
|
||||||
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
|
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
|
||||||
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
|
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const POD_STATUS_NOT_READY = 'POD_NOT_READY';
|
|
||||||
|
|
||||||
const POD_STATUS_FAILED = 'POD_FAILED';
|
const POD_STATUS_FAILED = 'POD_FAILED';
|
||||||
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
|
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
|
||||||
@@ -742,6 +740,7 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
if (this &&
|
if (this &&
|
||||||
!this.isVMExpectedRunning &&
|
!this.isVMExpectedRunning &&
|
||||||
this.isVMCreated &&
|
this.isVMCreated &&
|
||||||
|
this.vmi?.status?.phase !== undefined &&
|
||||||
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
|
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
|
||||||
this.vmi?.status?.phase !== VMIPhase.Pending
|
this.vmi?.status?.phase !== VMIPhase.Pending
|
||||||
) {
|
) {
|
||||||
@@ -1165,6 +1164,13 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const restoreConditions = this.restoreResource?.status?.conditions || [];
|
||||||
|
const restoreError = restoreConditions.find((s) => s.error === true && s.reason === 'Error' && s.type === 'Ready');
|
||||||
|
|
||||||
|
if (restoreError?.message) {
|
||||||
|
return restoreError.message;
|
||||||
|
}
|
||||||
|
|
||||||
return this.ingoreVMMessage ? '' : super.stateDescription;
|
return this.ingoreVMMessage ? '' : super.stateDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.6.0-rc3",
|
"version": "1.6.0-rc6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const docLink = (suffix, version) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getVersion(v) {
|
export function getVersion(v) {
|
||||||
// e.g v1.4.0
|
|
||||||
if (process.env.VUE_APP_SERVER_VERSION) {
|
if (process.env.VUE_APP_SERVER_VERSION) {
|
||||||
return process.env.VUE_APP_SERVER_VERSION;
|
return process.env.VUE_APP_SERVER_VERSION;
|
||||||
}
|
}
|
||||||
@@ -18,20 +17,21 @@ export function getVersion(v) {
|
|||||||
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
|
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// fallback to the latest version
|
// fallback to the latest version
|
||||||
return latestVersion(Object.keys(RELEASE_FEATURES));
|
return getLatestVersion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function latestVersion(versions) {
|
function getLatestVersion() {
|
||||||
return versions.sort((a, b) => semver.compare(a, b)).pop();
|
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare);
|
||||||
|
|
||||||
|
return allVersions[0] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// v1.3.3 => latest v1.3.x, v1.4.2 => latest v1.4.x
|
function getLatestCompatibleVersion(version) {
|
||||||
function latestMinorVersion(v) {
|
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid);
|
||||||
const minor = `v${ semver.major(v) }.${ semver.minor(v) }`;
|
const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare);
|
||||||
const minorVersions = Object.keys(RELEASE_FEATURES).filter((version) => version.startsWith(minor));
|
|
||||||
|
|
||||||
return latestVersion(minorVersions);
|
return compatible[0] || getLatestVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const featureEnabled = (featureKey, serverVersion) => {
|
export const featureEnabled = (featureKey, serverVersion) => {
|
||||||
@@ -46,12 +46,16 @@ export const featureEnabled = (featureKey, serverVersion) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let releasedFeatures = RELEASE_FEATURES[version];
|
const compatibleVersion = getLatestCompatibleVersion(version);
|
||||||
|
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
|
||||||
|
|
||||||
if (!releasedFeatures) {
|
if (!Array.isArray(releasedFeatures)) {
|
||||||
const fallback = latestMinorVersion(version);
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
`Feature flags for version ${ version } are not defined. Please upgrade Harvester UI extension and check the support matrix.`
|
||||||
|
);
|
||||||
|
|
||||||
releasedFeatures = RELEASE_FEATURES[fallback];
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return releasedFeatures.includes(featureKey);
|
return releasedFeatures.includes(featureKey);
|
||||||
|
|||||||
@@ -3133,15 +3133,15 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||||
|
|
||||||
"@rancher/icons@2.0.29":
|
"@rancher/icons@2.0.37":
|
||||||
version "2.0.29"
|
version "2.0.37"
|
||||||
resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.29.tgz#6546d69768c706bebd66bfa73b36aef3d105987a"
|
resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.37.tgz#61dd947009a4a3d27b2e4dca630da05583925744"
|
||||||
integrity sha512-qBBqfazS9y5VjV7fJDPNXmxd9AP/2uiE05mKFWP41kpbO+tEb62RnUBXCm14XLeScDZQcOuiAKVHMmvCFzF0BA==
|
integrity sha512-T2FqiZraz9yte9tNMCGqIDQVPwXKTAPUTE8eAaQjhOJEbwfk/ttXDIKGYZ9mHApJX6s8JYVXAbRPcgXgc4zKpA==
|
||||||
|
|
||||||
"@rancher/shell@3.0.5-rc.6":
|
"@rancher/shell@3.0.5-rc.7":
|
||||||
version "3.0.5-rc.6"
|
version "3.0.5-rc.7"
|
||||||
resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.5-rc.6.tgz#8d842cf1b0f7e3e52af53538953cf9dc5bfd0639"
|
resolved "https://registry.yarnpkg.com/@rancher/shell/-/shell-3.0.5-rc.7.tgz#a3c3f555c9459b312546fb577e9916cd48126fb2"
|
||||||
integrity sha512-B9+2WE1a06wRvCN7xvxp/HBdjrib04szIbc1pNBeMdDNCqQmZMvt2dftjYb44Tvg+4Ji8G+tcK8Q3Rry6NDJsw==
|
integrity sha512-YxiRteWwTnuCGEh2AqIyH6Vx/PJXSwugi7dpf2U+XnQLqQHYM8pFo38jG8Tmj95KqZsRMLXdesRow/Icq96D5w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@aws-sdk/client-ec2" "3.658.1"
|
"@aws-sdk/client-ec2" "3.658.1"
|
||||||
"@aws-sdk/client-eks" "3.1.0"
|
"@aws-sdk/client-eks" "3.1.0"
|
||||||
@@ -3153,7 +3153,7 @@
|
|||||||
"@babel/preset-typescript" "7.16.7"
|
"@babel/preset-typescript" "7.16.7"
|
||||||
"@novnc/novnc" "1.2.0"
|
"@novnc/novnc" "1.2.0"
|
||||||
"@popperjs/core" "2.11.8"
|
"@popperjs/core" "2.11.8"
|
||||||
"@rancher/icons" "2.0.29"
|
"@rancher/icons" "2.0.37"
|
||||||
"@types/is-url" "1.2.30"
|
"@types/is-url" "1.2.30"
|
||||||
"@types/node" "20.10.8"
|
"@types/node" "20.10.8"
|
||||||
"@types/semver" "^7.5.8"
|
"@types/semver" "^7.5.8"
|
||||||
|
|||||||
Reference in New Issue
Block a user