mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 13:11:43 +00:00
Latest changes from Harvester master - a537c1ae38eb7030542ac371f24ae3336cd9d422
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
parent
2ca86930ad
commit
118aaf16b7
@ -16,6 +16,7 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.10.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.9",
|
||||
"follow-redirects": "1.15.2",
|
||||
|
||||
123
pkg/harvester/components/FilterVMSchedule.vue
Normal file
123
pkg/harvester/components/FilterVMSchedule.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<script>
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterFilterVMSchedule',
|
||||
|
||||
emits: ['change-rows'],
|
||||
|
||||
components: { RadioGroup },
|
||||
|
||||
props: {
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selected: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
scheduleOptions() {
|
||||
const options = this.rows.map(r => r.sourceSchedule).filter(r => r);
|
||||
|
||||
return Array.from(new Set(options));
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSelect(selected) {
|
||||
this.selected = selected;
|
||||
this.filterRows();
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.selected = '';
|
||||
this.filterRows();
|
||||
},
|
||||
|
||||
filterRows() {
|
||||
if (!this.selected) {
|
||||
this.$emit('change-rows', this.rows);
|
||||
|
||||
return;
|
||||
}
|
||||
const filteredRows = this.rows.filter(row => row.sourceSchedule === this.selected);
|
||||
|
||||
this.$emit('change-rows', filteredRows, this.selected);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
rows: {
|
||||
deep: true,
|
||||
immediate: false,
|
||||
handler() {
|
||||
this.filterRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vm-schedule-filter">
|
||||
<template>
|
||||
<span v-if="selected" class="banner-item bg-warning">
|
||||
{{ t('harvester.tableHeaders.vmSchedule') }}{{ selected ? ` = ${selected}`: '' }}<i class="icon icon-close ml-5" @click="remove" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<v-dropdown
|
||||
:triggers="scheduleOptions.length ? ['click'] : []"
|
||||
placement="bottom-end"
|
||||
offset="1"
|
||||
>
|
||||
<button ref="actionDropDown" class="btn bg-primary mr-10">
|
||||
<slot name="title">
|
||||
{{ t('harvester.fields.filterSchedule') }}
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
<template #popper>
|
||||
<div class="filter-popup">
|
||||
<RadioGroup
|
||||
v-model:value="selected"
|
||||
class="mr-10 ml-10"
|
||||
name="model"
|
||||
:options="scheduleOptions"
|
||||
:labels="scheduleOptions"
|
||||
@input="onSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vm-schedule-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: max-content;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -398,6 +398,7 @@ export function init($plugin, store) {
|
||||
|
||||
basicType(
|
||||
[
|
||||
HCI.SCHEDULE_VM_BACKUP,
|
||||
HCI.BACKUP,
|
||||
HCI.SNAPSHOT,
|
||||
HCI.VM_SNAPSHOT,
|
||||
@ -445,6 +446,20 @@ export function init($plugin, store) {
|
||||
exact: false
|
||||
});
|
||||
|
||||
configureType(HCI.SCHEDULE_VM_BACKUP, { showListMasthead: false, showConfigView: false });
|
||||
virtualType({
|
||||
labelKey: 'harvester.schedule.label',
|
||||
name: HCI.SCHEDULE_VM_BACKUP,
|
||||
namespaced: true,
|
||||
weight: 201,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.SCHEDULE_VM_BACKUP }
|
||||
},
|
||||
exact: false,
|
||||
ifHaveType: HCI.SCHEDULE_VM_BACKUP,
|
||||
});
|
||||
|
||||
configureType(HCI.BACKUP, { showListMasthead: false, showConfigView: false });
|
||||
virtualType({
|
||||
labelKey: 'harvester.backup.label',
|
||||
|
||||
@ -53,4 +53,5 @@ export const HCI = {
|
||||
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
|
||||
CPU_MANAGER: 'cpumanager',
|
||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||
};
|
||||
|
||||
@ -40,3 +40,40 @@ export const SNAPSHOT_TARGET_VOLUME = {
|
||||
sort: 'spec.source.persistentVolumeClaimName',
|
||||
formatter: 'SnapshotTargetVolume',
|
||||
};
|
||||
|
||||
// The column of cron expression volume on VM schedules list page
|
||||
export const VM_SCHEDULE_CRON = {
|
||||
name: 'CronExpression',
|
||||
labelKey: 'harvester.tableHeaders.cronExpression',
|
||||
value: 'spec.cron',
|
||||
align: 'center',
|
||||
sort: 'spec.cron',
|
||||
formatter: 'HarvesterCronExpression',
|
||||
};
|
||||
|
||||
// The column of retain on VM schedules list page
|
||||
export const VM_SCHEDULE_RETAIN = {
|
||||
name: 'Retain',
|
||||
labelKey: 'harvester.tableHeaders.retain',
|
||||
value: 'spec.retain',
|
||||
sort: 'spec.retain',
|
||||
align: 'center',
|
||||
};
|
||||
|
||||
// The column of maxFailure on VM schedules list page
|
||||
export const VM_SCHEDULE_MAX_FAILURE = {
|
||||
name: 'MaxFailure',
|
||||
labelKey: 'harvester.tableHeaders.maxFailure',
|
||||
value: 'spec.maxFailure',
|
||||
sort: 'spec.maxFailure',
|
||||
align: 'center',
|
||||
};
|
||||
|
||||
// The column of type on VM schedules list page
|
||||
export const VM_SCHEDULE_TYPE = {
|
||||
name: 'Type',
|
||||
labelKey: 'harvester.tableHeaders.scheduleType',
|
||||
value: 'spec.vmbackup.type',
|
||||
sort: 'spec.vmbackup.type',
|
||||
align: 'center',
|
||||
};
|
||||
|
||||
@ -0,0 +1,129 @@
|
||||
<script>
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { STATE, NAME, AGE } from '@shell/config/table-headers';
|
||||
import { allSettled } from '../../utils/promise';
|
||||
import { BACKUP_TYPE } from '../../config/types';
|
||||
import { HCI } from '../../types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
export default {
|
||||
name: 'BackupList',
|
||||
|
||||
components: { ResourceTable },
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const hash = await allSettled({ backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }) });
|
||||
|
||||
this.rows = hash.backups;
|
||||
},
|
||||
|
||||
data() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const schema = this.$store.getters[`${ inStore }/schemaFor`](HCI.BACKUP);
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
schema
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
headers() {
|
||||
const cols = [
|
||||
STATE,
|
||||
{
|
||||
...NAME,
|
||||
width: 400
|
||||
},
|
||||
{
|
||||
name: 'targetVM',
|
||||
labelKey: 'tableHeaders.targetVm',
|
||||
value: 'attachVM',
|
||||
align: 'left',
|
||||
sort: 'attachVM',
|
||||
formatter: 'AttachVMWithName'
|
||||
},
|
||||
{
|
||||
name: 'backupTarget',
|
||||
labelKey: 'tableHeaders.backupTarget',
|
||||
value: 'backupTarget',
|
||||
sort: 'backupTarget',
|
||||
align: 'left',
|
||||
formatter: 'HarvesterBackupTargetValidation'
|
||||
},
|
||||
{
|
||||
name: 'readyToUse',
|
||||
labelKey: 'tableHeaders.readyToUse',
|
||||
value: 'status.readyToUse',
|
||||
sort: 'status.readyToUse',
|
||||
align: 'center',
|
||||
formatter: 'Checked',
|
||||
},
|
||||
];
|
||||
|
||||
if (this.hasBackupProgresses) {
|
||||
cols.push({
|
||||
name: 'backupProgress',
|
||||
labelKey: 'tableHeaders.progress',
|
||||
value: 'backupProgress',
|
||||
sort: 'backupProgress',
|
||||
align: 'left',
|
||||
formatter: 'HarvesterBackupProgressBar',
|
||||
});
|
||||
}
|
||||
cols.push(AGE);
|
||||
|
||||
return cols;
|
||||
},
|
||||
|
||||
hasBackupProgresses() {
|
||||
return !!this.rows.find(R => R.status?.progress !== undefined);
|
||||
},
|
||||
|
||||
filteredRows() {
|
||||
let r = this.rows.filter(row => row.spec?.type === BACKUP_TYPE.BACKUP);
|
||||
|
||||
if (this.id) {
|
||||
r = r.filter(backup => backup.metadata.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID] === this.id);
|
||||
}
|
||||
|
||||
return r;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResourceTable
|
||||
v-bind="$attrs"
|
||||
:headers="headers"
|
||||
:groupable="false"
|
||||
:rows="filteredRows"
|
||||
:schema="schema"
|
||||
key-field="_key"
|
||||
default-sort-by="age"
|
||||
>
|
||||
<template #col:name="{row}">
|
||||
<td>
|
||||
<span>
|
||||
<router-link
|
||||
v-if="row?.status?.source"
|
||||
:to="row.detailLocation"
|
||||
>
|
||||
{{ row.nameDisplay }}
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</template>
|
||||
@ -0,0 +1,97 @@
|
||||
<script>
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { STATE, NAME, AGE } from '@shell/config/table-headers';
|
||||
import { allSettled } from '../../utils/promise';
|
||||
import { BACKUP_TYPE } from '../../config/types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { HCI } from '../../types';
|
||||
import { schema } from '../../list/harvesterhci.io.vmsnapshot';
|
||||
|
||||
export default {
|
||||
name: 'SnapshotList',
|
||||
|
||||
components: { ResourceTable },
|
||||
|
||||
props: {
|
||||
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
async fetch() {
|
||||
const hash = await allSettled({ backups: this.$store.dispatch('harvester/findAll', { type: HCI.BACKUP }) });
|
||||
|
||||
this.rows = hash.backups;
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
rows: [],
|
||||
schema
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
headers() {
|
||||
return [
|
||||
STATE,
|
||||
NAME,
|
||||
{
|
||||
name: 'targetVM',
|
||||
labelKey: 'tableHeaders.targetVm',
|
||||
value: 'attachVM',
|
||||
align: 'left',
|
||||
formatter: 'AttachVMWithName'
|
||||
},
|
||||
{
|
||||
name: 'readyToUse',
|
||||
labelKey: 'tableHeaders.readyToUse',
|
||||
value: 'status.readyToUse',
|
||||
align: 'center',
|
||||
formatter: 'Checked',
|
||||
},
|
||||
AGE
|
||||
];
|
||||
},
|
||||
|
||||
filteredRows() {
|
||||
let r = this.rows.filter(row => row.spec?.type === BACKUP_TYPE.SNAPSHOT);
|
||||
|
||||
if (this.id) {
|
||||
r = r.filter(row => row.metadata.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID] === this.id);
|
||||
}
|
||||
|
||||
return r;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResourceTable
|
||||
v-bind="$attrs"
|
||||
:headers="headers"
|
||||
:groupable="false"
|
||||
:rows="filteredRows"
|
||||
:schema="schema"
|
||||
key-field="_key"
|
||||
default-sort-by="age"
|
||||
>
|
||||
<template #col:name="{row}">
|
||||
<td>
|
||||
<span>
|
||||
<router-link
|
||||
v-if="row?.status?.source"
|
||||
:to="row.detailLocation"
|
||||
>
|
||||
{{ row.nameDisplay }}
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</template>
|
||||
125
pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue
Normal file
125
pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue
Normal file
@ -0,0 +1,125 @@
|
||||
<script>
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { BACKUP_TYPE } from '../../config/types';
|
||||
import BackupList from './BackupList';
|
||||
import SnapshotList from './SnapshotList';
|
||||
import cronstrue from 'cronstrue';
|
||||
|
||||
export default {
|
||||
name: 'ScheduleVmBackupDetail',
|
||||
components: {
|
||||
BackupList,
|
||||
SnapshotList,
|
||||
Tab,
|
||||
Tabbed,
|
||||
LabelValue,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isBackup() {
|
||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP;
|
||||
},
|
||||
isSnapshot() {
|
||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.SNAPSHOT;
|
||||
},
|
||||
cronExpression() {
|
||||
let cronHint = '';
|
||||
|
||||
try {
|
||||
cronHint = cronstrue.toString(this.value.spec.cron, { verbose: true });
|
||||
} catch (e) {
|
||||
cronHint = this.t('generic.invalidCron');
|
||||
}
|
||||
|
||||
return cronHint ? `${ this.value.spec.cron } (${ cronHint })` : this.value.spec.cron;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||
<Tab
|
||||
name="basic"
|
||||
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
||||
class="bordered-table"
|
||||
:weight="99"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue
|
||||
:name="t('harvester.schedule.detail.namespace')"
|
||||
:value="value.metadata.namespace"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue
|
||||
:name="t('harvester.schedule.detail.sourceVM')"
|
||||
:value="value.spec.vmbackup.source.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue
|
||||
:name="t('harvester.schedule.cron')"
|
||||
:value="cronExpression"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue
|
||||
:name="t('harvester.schedule.scheduleType')"
|
||||
:value="value.spec.vmbackup.type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue
|
||||
:name="t('harvester.schedule.retain.label')"
|
||||
:value="value.spec.retain"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue
|
||||
:name="t('harvester.schedule.maxFailure.label')"
|
||||
:value="value.spec.maxFailure"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="isBackup"
|
||||
name="backups"
|
||||
:label="t('harvester.schedule.tabs.backups')"
|
||||
:weight="89"
|
||||
class="bordered-table"
|
||||
>
|
||||
<BackupList :id="value.id" />
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="isSnapshot"
|
||||
name="snapshots"
|
||||
:label="t('harvester.schedule.tabs.snapshots')"
|
||||
:weight="79"
|
||||
class="bordered-table"
|
||||
>
|
||||
<SnapshotList :id="value.id" />
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
@ -127,29 +127,29 @@ export default {
|
||||
<Tabbed v-if="spec" :side-tabs="true" @changed="onTabChanged">
|
||||
<Tab name="Basics" :label="t('harvester.virtualMachine.detail.tabs.basics')">
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.name')" :value="name" />
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue :name="t('harvester.fields.image')" :value="imageName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.hostname')" :value="hostname" />
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue :name="t('harvester.virtualMachine.input.MachineType')" :value="machineType" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
|
||||
|
||||
<div class="mb-20">
|
||||
<CpuMemory :cpu="cpu" :mode="mode" :memory="memory" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.bootOrder')">
|
||||
<template #value>
|
||||
<div>
|
||||
@ -162,7 +162,7 @@ export default {
|
||||
</template>
|
||||
</LabelValue>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<div class="col span-6 mb-20">
|
||||
<LabelValue :name="t('harvester.virtualMachine.detail.details.CDROMs')">
|
||||
<template #value>
|
||||
<div>
|
||||
@ -180,7 +180,6 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="volume"
|
||||
:label="t('harvester.tab.volume')"
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
<script>
|
||||
import { HCI } from '../types';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Card } from '@components/Card';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { escapeHtml } from '@shell/utils/string';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEnableUSBPassthrough',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton,
|
||||
Card,
|
||||
@ -83,25 +85,28 @@ export default {
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<h4
|
||||
slot="title"
|
||||
v-clean-html="t('promptRemove.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
|
||||
<template #body>
|
||||
<t k="harvester.usb.enablePassthroughWarning" :raw="true" />
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('promptRemove.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<template #body>
|
||||
{{ t('harvester.usb.enablePassthroughWarning') }}
|
||||
</template>
|
||||
|
||||
<AsyncButton mode="enable" @click="save" />
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button class="btn role-secondary mr-10" @click="close">
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton mode="enable" @click="save" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
|
||||
@ -171,7 +171,6 @@ export default {
|
||||
@click="apply"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner v-for="(err, i) in errors" :key="i" color="error" :label="err" />
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
298
pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue
Normal file
298
pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue
Normal file
@ -0,0 +1,298 @@
|
||||
<script>
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { isCronValid } from '@pkg/harvester/utils/cron';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../types';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||
|
||||
export default {
|
||||
name: 'CreateVMSchedule',
|
||||
components: {
|
||||
CruResource,
|
||||
Tabbed,
|
||||
Tab,
|
||||
RadioGroup,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
MessageLink,
|
||||
Banner,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
const hash = await allHash({
|
||||
settings: this.$store.dispatch('harvester/findAll', { type: HCI.SETTING }),
|
||||
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||
});
|
||||
|
||||
this.allVms = hash.vms;
|
||||
this.settings = hash.settings;
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
if (this.mode === _CREATE) {
|
||||
const defaultNs = this.$store.getters['defaultNamespace'];
|
||||
const vmNamespace = this.$route.query?.vmNamespace || defaultNs;
|
||||
const vmName = this.$route.query?.vmName;
|
||||
|
||||
delete this.value.metadata.annotations;
|
||||
delete this.value.metadata.labels;
|
||||
|
||||
this.value['metadata'] = {
|
||||
namespace: vmNamespace,
|
||||
name: vmName ? `svmbackup-${ vmName }` : ''
|
||||
};
|
||||
|
||||
if (!this.value.spec) {
|
||||
this.value['spec'] = {
|
||||
cron: '',
|
||||
retain: 8,
|
||||
maxFailure: 4,
|
||||
vmbackup: {
|
||||
source: {
|
||||
apiGroup: 'kubevirt.io',
|
||||
kind: 'VirtualMachine',
|
||||
name: vmName || ''
|
||||
},
|
||||
type: BACKUP_TYPE.BACKUP
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { settings: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
backupTargetResource() {
|
||||
return this.settings.find( O => O.id === 'backup-target');
|
||||
},
|
||||
isEmptyValue() {
|
||||
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
||||
},
|
||||
canUpdate() {
|
||||
return this?.backupTargetResource?.canUpdate;
|
||||
},
|
||||
errorMessage() {
|
||||
return this.backupTargetResource?.errMessage;
|
||||
},
|
||||
canSave() {
|
||||
return !!this.value.spec.cron && isCronValid(this.value.spec.cron) &&
|
||||
!!this.value.metadata.name &&
|
||||
!!this.value.metadata.namespace &&
|
||||
!!this.value.spec.retain &&
|
||||
!!this.value.spec.maxFailure;
|
||||
},
|
||||
isBackupTargetUnAvailable() {
|
||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
|
||||
},
|
||||
vmOptions() {
|
||||
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter(vm => vm.metadata.namespace === this.value.metadata.namespace);
|
||||
const vmObjectLists = nsVmList.map(obj => ({
|
||||
label: obj.nameDisplay,
|
||||
value: obj.name,
|
||||
}));
|
||||
|
||||
return sortBy(vmObjectLists, 'label');
|
||||
},
|
||||
|
||||
namespaces() {
|
||||
const allNamespaces = this.$store.getters['allNamespaces'];
|
||||
const out = sortBy(
|
||||
allNamespaces.map((obj) => {
|
||||
return {
|
||||
label: obj.nameDisplay,
|
||||
value: obj.id,
|
||||
};
|
||||
}),
|
||||
'label'
|
||||
);
|
||||
|
||||
return out;
|
||||
},
|
||||
toBackupTargetSetting() {
|
||||
const { cluster } = this.$router?.currentRoute?.params || {};
|
||||
|
||||
return {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-id`,
|
||||
params: {
|
||||
resource: `${ HCI.SETTING }`,
|
||||
cluster,
|
||||
id: 'backup-target'
|
||||
},
|
||||
query: { mode: _EDIT }
|
||||
};
|
||||
},
|
||||
scheduleTypeOptions() {
|
||||
return [BACKUP_TYPE.BACKUP, BACKUP_TYPE.SNAPSHOT];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.metadata.namespace'() {
|
||||
this.value.spec.vmbackup.source.name = '';
|
||||
},
|
||||
'value.spec.vmbackup.source.name'(neu) {
|
||||
this.value.metadata.name = `svm${ this.value.spec.vmbackup.type }-${ neu }`;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTypeChange(newType) {
|
||||
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
|
||||
},
|
||||
getBackupTargetValueIsEmpty(resource) {
|
||||
let out = true;
|
||||
|
||||
if (resource?.value) {
|
||||
try {
|
||||
const valueJson = JSON.parse(resource?.value);
|
||||
|
||||
out = !valueJson.type;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
validateFailure(count) {
|
||||
if (this.value.spec.retain && count > this.value.spec.retain) {
|
||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CruResource
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
:validationPassed="canSave"
|
||||
@finish="save"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<div class="banner">
|
||||
<Banner
|
||||
v-if="isBackupTargetUnAvailable"
|
||||
color="error"
|
||||
>
|
||||
<MessageLink
|
||||
v-if="isEmptyValue"
|
||||
:to="toBackupTargetSetting"
|
||||
:target="_blank"
|
||||
prefix-label="harvester.backup.message.noSetting.prefix"
|
||||
middle-label="harvester.backup.message.noSetting.middle"
|
||||
suffix-label="harvester.schedule.message.noSetting.suffix"
|
||||
/>
|
||||
<MessageLink
|
||||
v-else
|
||||
:to="toBackupTargetSetting"
|
||||
prefix-label="harvester.backup.message.errorTip.prefix"
|
||||
middle-label="harvester.backup.message.errorTip.middle"
|
||||
>
|
||||
<template v-slot:suffix>
|
||||
{{ t('harvester.backup.message.errorTip.suffix') }} {{ errorMessage }}
|
||||
</template>
|
||||
</MessageLink>
|
||||
</Banner>
|
||||
<div class="mb-30">
|
||||
<RadioGroup
|
||||
v-model:value="value.spec.vmbackup.type"
|
||||
name="model"
|
||||
:options="scheduleTypeOptions"
|
||||
:labels="[t('harvester.schedule.type.backup'), t('harvester.schedule.type.snapshot')]"
|
||||
:disabled="isEdit || isView"
|
||||
:mode="mode"
|
||||
row
|
||||
@input="onTypeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="row mb-30">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.metadata.namespace"
|
||||
:label="t('nameNsDescription.namespace.label')"
|
||||
:options="namespaces"
|
||||
required
|
||||
:disabled="isBackupTargetUnAvailable || isEdit || isView"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model:value="value.spec.vmbackup.source.name"
|
||||
:label="t('harvester.schedule.virtualMachine.title')"
|
||||
:placeholder="t('harvester.schedule.virtualMachine.placeholder')"
|
||||
:options="vmOptions"
|
||||
required
|
||||
:disabled="isBackupTargetUnAvailable || isEdit || isView"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabbed v-bind="$attrs" class="mt-15" :side-tabs="true">
|
||||
<Tab name="basics" :label="t('harvester.network.tabs.basics')" :weight="99" class="bordered-table">
|
||||
<LabeledInput
|
||||
v-model:value="value.spec.cron"
|
||||
class="mb-30"
|
||||
type="cron"
|
||||
required
|
||||
:mode="mode"
|
||||
:label="t('harvester.schedule.cron')"
|
||||
placeholder="0 * * * *"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value.number="value.spec.retain"
|
||||
v-int-number
|
||||
class="mb-30"
|
||||
:min="2"
|
||||
:max="250"
|
||||
type="number"
|
||||
:label="t('harvester.schedule.retain.label')"
|
||||
required
|
||||
:tooltip="t('harvester.schedule.retain.tooltip')"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value.number="value.spec.maxFailure"
|
||||
v-int-number
|
||||
class="mb-30"
|
||||
:min="2"
|
||||
type="number"
|
||||
:label="t('harvester.schedule.maxFailure.label')"
|
||||
required
|
||||
:tooltip="t('harvester.schedule.maxFailure.tooltip')"
|
||||
:disabled="isBackupTargetUnAvailable || isView"
|
||||
@input="validateFailure"
|
||||
/>
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</CruResource>
|
||||
</template>
|
||||
@ -159,6 +159,10 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancelAction() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
|
||||
async saveRestore(buttonCb) {
|
||||
this.update();
|
||||
|
||||
@ -261,7 +265,7 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="done" />
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="cancelAction" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -157,6 +157,9 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancelAction() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
async saveRestore(buttonCb) {
|
||||
this.update();
|
||||
|
||||
@ -241,7 +244,7 @@ export default {
|
||||
<LabeledSelect v-if="!restoreNewVm" v-model:value="deletionPolicy" :label="t('harvester.backup.restore.deletePreviousVolumes')" :options="deletionPolicyOption" />
|
||||
</div>
|
||||
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="done" />
|
||||
<Footer mode="create" class="footer" :errors="errors" @save="saveRestore" @done="cancelAction" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -225,13 +225,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
headerFor(type) {
|
||||
return {
|
||||
headerFor(type, hasVolBackups = false) {
|
||||
const mainHeader = {
|
||||
[SOURCE_TYPE.NEW]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.volume'),
|
||||
[SOURCE_TYPE.IMAGE]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.vmImage'),
|
||||
[SOURCE_TYPE.ATTACH_VOLUME]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.existingVolume'),
|
||||
[SOURCE_TYPE.CONTAINER]: this.$store.getters['i18n/t']('harvester.virtualMachine.volume.title.container'),
|
||||
}[type];
|
||||
|
||||
return hasVolBackups ? `${ mainHeader } and Backups` : mainHeader;
|
||||
},
|
||||
|
||||
update() {
|
||||
@ -322,7 +324,7 @@ export default {
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ headerFor(volume.source) }}
|
||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||
</span>
|
||||
</h3>
|
||||
<div>
|
||||
|
||||
@ -3,6 +3,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
import { Banner } from '@components/Banner';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterEditContainer',
|
||||
@ -10,7 +11,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
LabeledInput, LabeledSelect, InputOrDisplay
|
||||
LabeledInput, LabeledSelect, InputOrDisplay, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -70,7 +71,6 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="input-hec-type"
|
||||
class="col span-6"
|
||||
@ -111,7 +111,6 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="input-hec-bus"
|
||||
class="col span-6"
|
||||
@ -131,5 +130,28 @@ export default {
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div
|
||||
v-if="value.volumeBackups"
|
||||
class="col span-6"
|
||||
>
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
:mode="mode"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<script>
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
|
||||
import { Banner } from '@components/Banner';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
@ -17,7 +18,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -275,7 +276,6 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="input-hee-bus"
|
||||
class="col span-6"
|
||||
@ -296,6 +296,27 @@ export default {
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div
|
||||
v-if="value.volumeBackups"
|
||||
class="col span-6"
|
||||
>
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
:mode="mode"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="value.volumeBackups.readyToUse"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -4,6 +4,7 @@ import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
import { HCI } from '../../../../types';
|
||||
@ -18,7 +19,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue
|
||||
UnitInput, LabeledInput, LabeledSelect, InputOrDisplay, LabelValue, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -101,6 +102,12 @@ export default {
|
||||
return image ? image.label : '-';
|
||||
},
|
||||
|
||||
readyToUse() {
|
||||
const val = String(this.value.volumeBackups?.readyToUse || false);
|
||||
|
||||
return ucFirst(val);
|
||||
},
|
||||
|
||||
pvcsResource() {
|
||||
const allPVCs = this.$store.getters['harvester/all'](PVC) || [];
|
||||
|
||||
@ -301,7 +308,7 @@ export default {
|
||||
</div>
|
||||
<div
|
||||
v-if="isView"
|
||||
class="col span-6"
|
||||
class="col span-3"
|
||||
>
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.encryption')"
|
||||
@ -309,5 +316,19 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div v-if="value.volumeBackups && isView" class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="readyToUse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -4,7 +4,7 @@ import UnitInput from '@shell/components/form/UnitInput';
|
||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
import { Banner } from '@components/Banner';
|
||||
import { PVC, STORAGE_CLASS } from '@shell/config/types';
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
@ -18,7 +18,7 @@ export default {
|
||||
emits: ['update'],
|
||||
|
||||
components: {
|
||||
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue
|
||||
InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue, Banner
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -68,9 +68,16 @@ export default {
|
||||
return ucFirst(String(this.value.isEncrypted));
|
||||
},
|
||||
|
||||
readyToUse() {
|
||||
const val = String(this.value.volumeBackups?.readyToUse || false);
|
||||
|
||||
return ucFirst(val);
|
||||
},
|
||||
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
pvcsResource() {
|
||||
const allPVCs = this.$store.getters['harvester/all'](PVC) || [];
|
||||
|
||||
@ -221,7 +228,6 @@ export default {
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div
|
||||
data-testid="input-hev-bus"
|
||||
@ -248,5 +254,19 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div v-if="value.volumeBackups && isView" class="col span-3">
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.volume.readyToUse')"
|
||||
:value="readyToUse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Banner
|
||||
v-if="value.volumeBackups && value.volumeBackups.error && value.volumeBackups.error.message"
|
||||
color="error"
|
||||
class="mb-20"
|
||||
:label="value.volumeBackups.error.message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import { isEqual } from 'lodash';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import Tabbed from '@shell/components/Tabbed';
|
||||
import Tab from '@shell/components/Tabbed/Tab';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
|
||||
41
pkg/harvester/formatters/BackupCreatedFrom.vue
Normal file
41
pkg/harvester/formatters/BackupCreatedFrom.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script>
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const scheduleList = this.$store.getters['harvester/all'](HCI.SCHEDULE_VM_BACKUP) || [];
|
||||
|
||||
return { scheduleList };
|
||||
},
|
||||
computed: {
|
||||
vmSchedule() {
|
||||
if (!this.value) {
|
||||
return '';
|
||||
} else {
|
||||
return this.scheduleList.find(s => s.id === this.value);
|
||||
}
|
||||
},
|
||||
to() {
|
||||
return this.vmSchedule?.detailLocation;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link v-if="to" :to="to">
|
||||
{{ value }}
|
||||
</router-link>
|
||||
<span v-else-if="value">
|
||||
{{ value }}
|
||||
</span>
|
||||
<span v-else class="text-muted">
|
||||
—
|
||||
</span>
|
||||
</template>
|
||||
32
pkg/harvester/formatters/HarvesterCronExpression.vue
Normal file
32
pkg/harvester/formatters/HarvesterCronExpression.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import cronstrue from 'cronstrue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cronTooltipHint() {
|
||||
let cronHint = '';
|
||||
|
||||
try {
|
||||
cronHint = cronstrue.toString(this.value, { verbose: true });
|
||||
} catch (e) {
|
||||
cronHint = this.t('generic.invalidCron');
|
||||
}
|
||||
|
||||
return cronHint || this.value.spec.cron;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-clean-tooltip="cronTooltipHint">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
@ -147,7 +147,7 @@ export default {
|
||||
>
|
||||
<template #title="{formattedPercentage}">
|
||||
<span>
|
||||
{{ t('harvester.formatters.hardwareResourceGauge.allocated') }}
|
||||
{{ t('harvester.dashboard.hardwareResourceGauge.allocated') }}
|
||||
</span>
|
||||
<span class="precent-data">
|
||||
{{ t('node.detail.glance.consumptionGauge.amount', allocatedAmountTemplateValues) }}
|
||||
|
||||
@ -84,6 +84,12 @@ harvester:
|
||||
tip: Please enter a template name!
|
||||
success: 'Template { templateName } created successfully.'
|
||||
failed: 'Failed generated template!'
|
||||
schedule:
|
||||
title: Create Schedule
|
||||
message:
|
||||
tip: Please enter a schedule name!
|
||||
success: 'Schedule { name } created successfully.'
|
||||
failed: 'Failed create schedule!'
|
||||
cloneVM:
|
||||
title: Clone Virtual Machine
|
||||
name: New Virtual Machine Name
|
||||
@ -171,8 +177,11 @@ harvester:
|
||||
setDefaultVersion: Set default version
|
||||
addTemplateVersion: Add template version
|
||||
backup: Take Backup
|
||||
createSchedule: Create Schedule
|
||||
restore: Restore
|
||||
restoreNewVM: Restore New
|
||||
resumeSchedule: Resume
|
||||
suspendSchedule: Suspend
|
||||
restoreExistingVM: Replace Existing
|
||||
migrate: Migrate
|
||||
abortMigration: Abort Migration
|
||||
@ -223,6 +232,12 @@ harvester:
|
||||
readyToUse: Ready To Use
|
||||
backupTarget: Backup Target
|
||||
targetVm: Target Virtual Machine
|
||||
cronExpression: Cron Expression
|
||||
retain: Retain
|
||||
scheduleType: Type
|
||||
maxFailure: Max Failure
|
||||
sourceVm: Source VM
|
||||
vmSchedule: Virtual Machine Schedule
|
||||
hostIp: Host IP
|
||||
vm:
|
||||
ipAddress: IP Address
|
||||
@ -269,6 +284,7 @@ harvester:
|
||||
promiscuous: Promiscuous
|
||||
ipv4Address: IPv4 address
|
||||
filterLabels: Filter labels
|
||||
filterSchedule: Filter schedule
|
||||
storageClass: Storage class
|
||||
dockerImage: Docker image
|
||||
pci:
|
||||
@ -356,6 +372,7 @@ harvester:
|
||||
cpu: CPU
|
||||
memory: Memory
|
||||
storage: Storage
|
||||
allocated: Allocated
|
||||
sections:
|
||||
events:
|
||||
label: Events
|
||||
@ -590,6 +607,7 @@ harvester:
|
||||
size: Size
|
||||
edit: Edit
|
||||
bus: Bus
|
||||
readyToUse: Ready To Use
|
||||
bootOrder: Boot Order
|
||||
volume: Volume
|
||||
dockerImage: Docker Image
|
||||
@ -848,6 +866,37 @@ harvester:
|
||||
doc: Read the <a href="{url}" target="_blank">documentation</a> before starting the upgrade process. Ensure that you complete procedures that are relevant to your environment and the version you are upgrading to.
|
||||
tip: Unmet system requirements and incorrectly performed procedures may cause complete upgrade failure and other issues that require manual workarounds.
|
||||
moreNotes: For more details about the release notes, please visit -
|
||||
|
||||
schedule:
|
||||
label: Virtual Machine Schedules
|
||||
createTitle: Create Schedule
|
||||
createButtonText: Create Schedule
|
||||
scheduleType: Virtual Machine Schedule Type
|
||||
cron: Cron Schedule
|
||||
detail:
|
||||
namespace: Namespace
|
||||
sourceVM: Source Virtual Machine
|
||||
tabs:
|
||||
basic: Basic
|
||||
backups: Backups
|
||||
snapshots: Snapshots
|
||||
message:
|
||||
noSetting:
|
||||
suffix: before creating a backup schedule
|
||||
retain:
|
||||
label: Retain
|
||||
count: Count
|
||||
tooltip: Number of up-to-date VM backups to retain. Maximum to 250, minimum to 2.
|
||||
maxFailure:
|
||||
label: Max Failure
|
||||
count: Count
|
||||
tooltip: Max number of consecutive failed backups that could be tolerated. If reach this threshold, Harvester controller will suspend the schedule job. This value should less than retain count
|
||||
virtualMachine:
|
||||
title: Virtual Machine Name
|
||||
placeholder: Select a virtual machine
|
||||
type:
|
||||
snapshot: Snapshot
|
||||
backup: Backup
|
||||
|
||||
backup:
|
||||
label: Virtual Machine Backups
|
||||
@ -886,7 +935,6 @@ harvester:
|
||||
starting: Backup initiating
|
||||
progress: Backup in progress
|
||||
complete: Backup completed
|
||||
|
||||
restore:
|
||||
progress:
|
||||
details: Volume details
|
||||
@ -1532,6 +1580,11 @@ typeLabel:
|
||||
one { Template }
|
||||
other { Templates }
|
||||
}
|
||||
harvesterhci.io.schedulevmbackup: |-
|
||||
{count, plural,
|
||||
one { Virtual Machine Schedule }
|
||||
other { Virtual Machine Schedules }
|
||||
}
|
||||
harvesterhci.io.virtualmachinebackup: |-
|
||||
{count, plural,
|
||||
one { Virtual Machines Backup }
|
||||
|
||||
@ -660,7 +660,7 @@ export default {
|
||||
:name="t('harvester.dashboard.hardwareResourceGauge.storage')"
|
||||
:used="storageUsed"
|
||||
:reserved="storageAllocated"
|
||||
:reserved-title="t('harvester.formatters.hardwareResourceGauge.allocated')"
|
||||
:reserved-title="t('harvester.dashboard.hardwareResourceGauge.allocated')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
123
pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue
Normal file
123
pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<script>
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { HCI } from '../types';
|
||||
import { allSettled } from '../utils/promise';
|
||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||
import { VM_SCHEDULE_CRON, VM_SCHEDULE_RETAIN, VM_SCHEDULE_TYPE, VM_SCHEDULE_MAX_FAILURE } from '../config/table-headers';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterListSchedule',
|
||||
components: {
|
||||
ResourceTable, Loading, Masthead,
|
||||
},
|
||||
|
||||
props: {
|
||||
schema: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const hash = await allSettled({
|
||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SCHEDULE_VM_BACKUP }),
|
||||
});
|
||||
|
||||
this.rows = hash.rows;
|
||||
},
|
||||
|
||||
data() {
|
||||
const params = { ...this.$route.params };
|
||||
const resource = params.resource;
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
settings: [],
|
||||
resource,
|
||||
to: `${ HCI.SETTING }/backup-target?mode=edit`,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
headers() {
|
||||
const cols = [
|
||||
STATE,
|
||||
NAME,
|
||||
NAMESPACE,
|
||||
{
|
||||
name: 'sourceVm',
|
||||
labelKey: 'harvester.tableHeaders.sourceVm',
|
||||
value: 'spec.vmbackup.source.name',
|
||||
sort: 'sourceVm',
|
||||
align: 'center',
|
||||
formatter: 'AttachVMWithName'
|
||||
},
|
||||
VM_SCHEDULE_TYPE,
|
||||
VM_SCHEDULE_CRON,
|
||||
VM_SCHEDULE_RETAIN,
|
||||
VM_SCHEDULE_MAX_FAILURE,
|
||||
AGE,
|
||||
];
|
||||
|
||||
return cols;
|
||||
},
|
||||
|
||||
filteredRows() {
|
||||
return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.SNAPSHOT);
|
||||
},
|
||||
|
||||
typeDisplay() {
|
||||
return this.t('harvester.schedule.label');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getRow(row) {
|
||||
return row.spec?.vmbackup?.source?.name;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else>
|
||||
<Masthead
|
||||
:schema="schema"
|
||||
:resource="resource"
|
||||
:type-display="typeDisplay"
|
||||
:parentNameOverride="'Virtual Machine schedule'"
|
||||
:create-button-label="t('harvester.schedule.createButtonText')"
|
||||
/>
|
||||
<ResourceTable
|
||||
v-bind="$attrs"
|
||||
:headers="headers"
|
||||
:groupable="true"
|
||||
:rows="filteredRows"
|
||||
:schema="schema"
|
||||
key-field="_key"
|
||||
default-sort-by="age"
|
||||
>
|
||||
<template #col:name="{row}">
|
||||
<td>
|
||||
<span>
|
||||
<router-link
|
||||
v-if="getRow(row)"
|
||||
:to="row.detailLocation"
|
||||
>
|
||||
{{ row.nameDisplay }}
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
</template>
|
||||
@ -6,34 +6,37 @@ import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
|
||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||
import FilterVMSchedule from '../components/FilterVMSchedule';
|
||||
import { HCI } from '../types';
|
||||
import { allSettled } from '../utils/promise';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterListBackup',
|
||||
components: {
|
||||
ResourceTable, Banner, Loading, Masthead, MessageLink
|
||||
ResourceTable, Banner, Loading, Masthead, MessageLink, FilterVMSchedule
|
||||
},
|
||||
|
||||
props: {
|
||||
schema: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const hash = await allSettled({
|
||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||
settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }),
|
||||
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
|
||||
vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }),
|
||||
settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }),
|
||||
backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
|
||||
scheduleList: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SCHEDULE_VM_BACKUP }),
|
||||
});
|
||||
|
||||
this.rows = hash.rows;
|
||||
this.backups = hash.backups;
|
||||
this.rows = hash.backups;
|
||||
this.settings = hash.settings;
|
||||
|
||||
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) {
|
||||
const backupTargetResource = hash.settings.find( O => O.id === 'backup-target');
|
||||
const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource);
|
||||
@ -50,10 +53,12 @@ export default {
|
||||
const resource = params.resource;
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
settings: [],
|
||||
rows: [],
|
||||
backups: [],
|
||||
settings: [],
|
||||
resource,
|
||||
to: `${ HCI.SETTING }/backup-target?mode=edit`,
|
||||
to: `${ HCI.SETTING }/backup-target?mode=edit`,
|
||||
searchSchedule: ''
|
||||
};
|
||||
},
|
||||
|
||||
@ -85,7 +90,25 @@ export default {
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
},
|
||||
|
||||
getRow(row) {
|
||||
return row.status && row.status.source;
|
||||
},
|
||||
|
||||
changeRows(filteredRows, searchSchedule) {
|
||||
this[searchSchedule] = searchSchedule;
|
||||
this[backups] = filteredRows;
|
||||
},
|
||||
|
||||
sortGenerationFn() {
|
||||
let base = defaultTableSortGenerationFn(this.schema, this.$store);
|
||||
|
||||
base += this.searchSchedule;
|
||||
|
||||
return base;
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
@ -101,6 +124,12 @@ export default {
|
||||
align: 'left',
|
||||
formatter: 'AttachVMWithName'
|
||||
},
|
||||
{
|
||||
name: 'backupCreatedFrom',
|
||||
labelKey: 'harvester.tableHeaders.vmSchedule',
|
||||
value: 'sourceSchedule',
|
||||
formatter: 'BackupCreatedFrom',
|
||||
},
|
||||
{
|
||||
name: 'backupTarget',
|
||||
labelKey: 'tableHeaders.backupTarget',
|
||||
@ -112,7 +141,7 @@ export default {
|
||||
name: 'readyToUse',
|
||||
labelKey: 'tableHeaders.readyToUse',
|
||||
value: 'status.readyToUse',
|
||||
align: 'left',
|
||||
align: 'center',
|
||||
formatter: 'Checked',
|
||||
},
|
||||
];
|
||||
@ -124,25 +153,24 @@ export default {
|
||||
value: 'backupProgress',
|
||||
align: 'left',
|
||||
formatter: 'HarvesterBackupProgressBar',
|
||||
width: 200,
|
||||
});
|
||||
}
|
||||
|
||||
cols.push(AGE);
|
||||
|
||||
return cols;
|
||||
},
|
||||
|
||||
hasBackupProgresses() {
|
||||
return !!this.rows.find(R => R.status?.progress !== undefined);
|
||||
return !!this.backups.find(r => r.status?.progress !== undefined);
|
||||
},
|
||||
|
||||
filteredRows() {
|
||||
return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.SNAPSHOT);
|
||||
return this.backups.filter(r => r.spec?.type !== BACKUP_TYPE.SNAPSHOT);
|
||||
},
|
||||
getRawRows() {
|
||||
return this.rows.filter(r => r.spec?.type === BACKUP_TYPE.BACKUP);
|
||||
},
|
||||
|
||||
backupTargetResource() {
|
||||
return this.settings.find( O => O.id === 'backup-target');
|
||||
return this.settings.find(O => O.id === 'backup-target');
|
||||
},
|
||||
|
||||
isEmptyValue() {
|
||||
@ -211,16 +239,22 @@ export default {
|
||||
:headers="headers"
|
||||
:groupable="true"
|
||||
:rows="filteredRows"
|
||||
:sort-generation-fn="sortGenerationFn"
|
||||
:schema="schema"
|
||||
key-field="_key"
|
||||
default-sort-by="age"
|
||||
|
||||
>
|
||||
<template #more-header-middle>
|
||||
<FilterVMSchedule
|
||||
:rows="getRawRows"
|
||||
@change-rows="changeRows"
|
||||
/>
|
||||
</template>
|
||||
<template #col:name="{row}">
|
||||
<td>
|
||||
<span>
|
||||
<router-link
|
||||
v-if="row.status && row.status.source"
|
||||
v-if="getRow(row)"
|
||||
:to="row.detailLocation"
|
||||
>
|
||||
{{ row.nameDisplay }}
|
||||
|
||||
@ -2,14 +2,15 @@
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
|
||||
import { SCHEMA } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import FilterVMSchedule from '../components/FilterVMSchedule';
|
||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||
import { HCI } from '../types';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
||||
|
||||
const schema = {
|
||||
export const schema = {
|
||||
id: HCI.VM_SNAPSHOT,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
@ -22,7 +23,7 @@ const schema = {
|
||||
export default {
|
||||
name: 'HarvesterListVMSnapshot',
|
||||
components: {
|
||||
ResourceTable, Loading, Masthead
|
||||
ResourceTable, Loading, Masthead, FilterVMSchedule
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
@ -39,6 +40,7 @@ export default {
|
||||
}
|
||||
|
||||
this.rows = hash.rows;
|
||||
this.snapshots = hash.rows;
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -47,7 +49,9 @@ export default {
|
||||
const resource = params.resource;
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
rows: [],
|
||||
snapshots: [],
|
||||
searchSchedule: '',
|
||||
resource,
|
||||
};
|
||||
},
|
||||
@ -63,19 +67,32 @@ export default {
|
||||
labelKey: 'tableHeaders.targetVm',
|
||||
value: 'attachVM',
|
||||
align: 'left',
|
||||
sort: 'attachVM',
|
||||
formatter: 'AttachVMWithName'
|
||||
},
|
||||
{
|
||||
name: 'backupCreatedFrom',
|
||||
labelKey: 'harvester.tableHeaders.vmSchedule',
|
||||
value: 'sourceSchedule',
|
||||
sort: 'sourceSchedule',
|
||||
formatter: 'BackupCreatedFrom',
|
||||
},
|
||||
{
|
||||
name: 'readyToUse',
|
||||
labelKey: 'tableHeaders.readyToUse',
|
||||
value: 'status.readyToUse',
|
||||
align: 'left',
|
||||
align: 'center',
|
||||
sort: 'status.readyToUse',
|
||||
formatter: 'Checked',
|
||||
},
|
||||
AGE
|
||||
];
|
||||
},
|
||||
|
||||
getRawRows() {
|
||||
return this.rows.filter(r => r.spec?.type === BACKUP_TYPE.SNAPSHOT);
|
||||
},
|
||||
|
||||
schema() {
|
||||
return schema;
|
||||
},
|
||||
@ -85,9 +102,24 @@ export default {
|
||||
},
|
||||
|
||||
filteredRows() {
|
||||
return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.BACKUP);
|
||||
return this.snapshots.filter(r => r.spec?.type !== BACKUP_TYPE.BACKUP);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeRows(filteredRows, searchSchedule) {
|
||||
this['searchSchedule'] = searchSchedule;
|
||||
this['snapshots'] = filteredRows;
|
||||
},
|
||||
|
||||
sortGenerationFn() {
|
||||
let base = defaultTableSortGenerationFn(this.schema, this.$store);
|
||||
|
||||
base += this.searchSchedule;
|
||||
|
||||
return base;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -100,17 +132,23 @@ export default {
|
||||
:type-display="typeDisplay"
|
||||
:create-button-label="t('harvester.vmSnapshot.createText')"
|
||||
/>
|
||||
|
||||
<ResourceTable
|
||||
v-bind="$attrs"
|
||||
:headers="headers"
|
||||
:groupable="true"
|
||||
:rows="filteredRows"
|
||||
:schema="schema"
|
||||
:sort-generation-fn="sortGenerationFn"
|
||||
key-field="_key"
|
||||
default-sort-by="age"
|
||||
|
||||
>
|
||||
<template #more-header-middle>
|
||||
<FilterVMSchedule
|
||||
:rows="getRawRows"
|
||||
@change-rows="changeRows"
|
||||
/>
|
||||
</template>
|
||||
<template #col:name="{row}">
|
||||
<td>
|
||||
<span>
|
||||
@ -126,6 +164,6 @@ export default {
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</resourcetable>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -301,6 +301,7 @@ export default {
|
||||
} = config;
|
||||
|
||||
const vm = this.resourceType === HCI.VM ? value : this.resourceType === HCI.BACKUP ? this.value.status?.source : value.spec.vm;
|
||||
const volumeBackups = this.resourceType === HCI.BACKUP ? this.value.status?.volumeBackups : null;
|
||||
|
||||
const spec = vm?.spec;
|
||||
|
||||
@ -335,7 +336,8 @@ export default {
|
||||
const sshKey = this.getSSHFromAnnotation(spec) || [];
|
||||
|
||||
const imageId = this.getRootImageId(vm) || '';
|
||||
const diskRows = this.getDiskRows(vm);
|
||||
const diskRows = this.getDiskRows(vm, volumeBackups);
|
||||
|
||||
const networkRows = this.getNetworkRows(vm, { fromTemplate, init });
|
||||
const hasCreateVolumes = this.getHasCreatedVolumes(spec) || [];
|
||||
|
||||
@ -404,7 +406,7 @@ export default {
|
||||
this.refreshYamlEditor();
|
||||
},
|
||||
|
||||
getDiskRows(vm) {
|
||||
getDiskRows(vm, volBackups) {
|
||||
const namespace = vm.metadata.namespace;
|
||||
const _volumes = vm.spec.template.spec.volumes || [];
|
||||
const _disks = vm.spec.template.spec.domain.devices.disks || [];
|
||||
@ -422,6 +424,7 @@ export default {
|
||||
const isIsoImage = /iso$/i.test(imageResource?.imageSuffix);
|
||||
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
||||
const isEncrypted = imageResource?.isEncrypted || false;
|
||||
const volumeBackups = volBackups?.find(vBackup => vBackup.volumeName === 'disk-0') || null ;
|
||||
|
||||
if (isIsoImage) {
|
||||
bus = 'sata';
|
||||
@ -449,7 +452,8 @@ export default {
|
||||
storageClassName: '',
|
||||
image: this.imageId,
|
||||
volumeMode: 'Block',
|
||||
isEncrypted
|
||||
isEncrypted,
|
||||
volumeBackups,
|
||||
});
|
||||
} else {
|
||||
out = _disks.map( (DISK, index) => {
|
||||
@ -531,6 +535,7 @@ export default {
|
||||
const volumeStatus = pvc?.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
|
||||
|
||||
const isEncrypted = pvc?.isEncrypted || false;
|
||||
const volumeBackups = volBackups?.find(vBackup => vBackup.volumeName === DISK.name) || null;
|
||||
|
||||
return {
|
||||
id: randomStr(5),
|
||||
@ -551,7 +556,8 @@ export default {
|
||||
volumeStatus,
|
||||
dataSource,
|
||||
namespace,
|
||||
isEncrypted
|
||||
isEncrypted,
|
||||
volumeBackups,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -1396,6 +1402,14 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
setCpuPinning(value) {
|
||||
if (value) {
|
||||
set(this.spec.template.spec.domain.cpu, 'dedicatedCpuPlacement', true);
|
||||
} else {
|
||||
this.$delete(this.spec.template.spec.domain.cpu, 'dedicatedCpuPlacement');
|
||||
}
|
||||
},
|
||||
|
||||
setTPM(tpmEnabled) {
|
||||
if (tpmEnabled) {
|
||||
set(this.spec.template.spec.domain.devices, 'tpm', {});
|
||||
|
||||
97
pkg/harvester/models/harvesterhci.io.schedulevmbackup.js
Normal file
97
pkg/harvester/models/harvesterhci.io.schedulevmbackup.js
Normal file
@ -0,0 +1,97 @@
|
||||
import HarvesterResource from './harvester';
|
||||
import { get } from '@shell/utils/object';
|
||||
import { findBy } from '@shell/utils/array';
|
||||
import { colorForState, stateDisplay, STATES } from '@shell/plugins/dashboard-store/resource-class';
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
import { ucFirst, escapeHtml } from '@shell/utils/string';
|
||||
|
||||
export default class ScheduleVmBackup extends HarvesterResource {
|
||||
detailPageHeaderActionOverride(realMode) {
|
||||
if (realMode === _CREATE) {
|
||||
return this.t('harvester.schedule.createTitle');
|
||||
}
|
||||
}
|
||||
|
||||
get _availableActions() {
|
||||
const toFilter = ['goToClone'];
|
||||
|
||||
const out = super._availableActions.filter((action) => {
|
||||
if (!toFilter.includes(action.action)) {
|
||||
return action;
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
action: 'resumeSchedule',
|
||||
enabled: ucFirst(this.state) === STATES.suspended.label,
|
||||
icon: 'icons icon-play',
|
||||
label: this.t('harvester.action.resumeSchedule'),
|
||||
},
|
||||
{
|
||||
action: 'suspendSchedule',
|
||||
enabled: ucFirst(this.state) === STATES.active.label,
|
||||
icon: 'icons icon-pause',
|
||||
label: this.t('harvester.action.suspendSchedule'),
|
||||
},
|
||||
...out
|
||||
];
|
||||
}
|
||||
|
||||
async suspendSchedule() {
|
||||
try {
|
||||
this.spec.suspend = true; // suspend schedule
|
||||
await this.save();
|
||||
} catch (err) {
|
||||
this.spec.suspend = false;
|
||||
|
||||
this.$dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
}
|
||||
}
|
||||
|
||||
async resumeSchedule() {
|
||||
try {
|
||||
this.spec.suspend = false; // resume schedule
|
||||
await this.save();
|
||||
} catch (err) {
|
||||
this.spec.suspend = true;
|
||||
|
||||
this.$dispatch('growl/fromError', {
|
||||
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
|
||||
err,
|
||||
}, { root: true });
|
||||
}
|
||||
}
|
||||
|
||||
get state() {
|
||||
const conditions = get(this, 'status.conditions');
|
||||
const isSuspended = findBy(conditions, 'type', 'BackupSuspend')?.status === 'True';
|
||||
|
||||
if (isSuspended) {
|
||||
return STATES.suspended.label;
|
||||
}
|
||||
|
||||
return this.metadata.state.name;
|
||||
}
|
||||
|
||||
get stateDescription() {
|
||||
const suspendedCondition = (this.status?.conditions || []).find(c => c.type === 'BackupSuspend');
|
||||
|
||||
return ucFirst(suspendedCondition?.message) || super.stateDescription;
|
||||
}
|
||||
|
||||
get stateBackground() {
|
||||
return colorForState(this.stateDisplay).replace('text-', 'bg-');
|
||||
}
|
||||
|
||||
get stateColor() {
|
||||
return colorForState(this.state);
|
||||
}
|
||||
|
||||
get stateDisplay() {
|
||||
return stateDisplay(this.state);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { get, clone } from '@shell/utils/object';
|
||||
import { findBy } from '@shell/utils/array';
|
||||
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { _CREATE } from '@shell/config/query-params';
|
||||
import { HCI } from '../types';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
@ -18,9 +19,8 @@ export default class HciVmBackup extends HarvesterResource {
|
||||
|
||||
get detailLocation() {
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
const route = this.currentRoute();
|
||||
|
||||
detailLocation.params.resource = route.params.resource;
|
||||
detailLocation.params.resource = HCI.BACKUP;
|
||||
|
||||
return detailLocation;
|
||||
}
|
||||
@ -81,24 +81,30 @@ export default class HciVmBackup extends HarvesterResource {
|
||||
}
|
||||
|
||||
restoreExistingVM(resource = this) {
|
||||
const route = this.currentRoute();
|
||||
const router = this.currentRouter();
|
||||
const targetResource = resource.spec.type === BACKUP_TYPE.BACKUP ? HCI.BACKUP : HCI.VM_SNAPSHOT;
|
||||
|
||||
router.push({
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||
params: { resource: route.params.resource },
|
||||
query: { restoreMode: 'existing', resourceName: resource.name }
|
||||
params: { resource: targetResource },
|
||||
query: {
|
||||
restoreMode: 'existing',
|
||||
resourceName: resource.name,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
restoreNewVM(resource = this) {
|
||||
const route = this.currentRoute();
|
||||
const router = this.currentRouter();
|
||||
const targetResource = resource.spec.type === BACKUP_TYPE.BACKUP ? HCI.BACKUP : HCI.VM_SNAPSHOT;
|
||||
|
||||
router.push({
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||
params: { resource: route.params.resource },
|
||||
query: { restoreMode: 'new', resourceName: resource.name }
|
||||
params: { resource: targetResource },
|
||||
query: {
|
||||
restoreMode: 'new',
|
||||
resourceName: resource.name,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -125,6 +131,10 @@ export default class HciVmBackup extends HarvesterResource {
|
||||
return colorForState(state);
|
||||
}
|
||||
|
||||
get sourceSchedule() {
|
||||
return this.metadata?.annotations[HCI_ANNOTATIONS.SVM_BACKUP_ID];
|
||||
}
|
||||
|
||||
get attachVM() {
|
||||
return this.spec.source.name;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { load } from 'js-yaml';
|
||||
import { omitBy, pickBy } from 'lodash';
|
||||
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
|
||||
import { POD, NODE, PVC } from '@shell/config/types';
|
||||
import { findBy } from '@shell/utils/array';
|
||||
@ -161,6 +161,12 @@ export default class VirtVm extends HarvesterResource {
|
||||
icon: 'icon icon-storage',
|
||||
label: this.t('harvester.action.editVMQuota')
|
||||
},
|
||||
{
|
||||
action: 'createSchedule',
|
||||
enabled: true,
|
||||
icon: 'icon icon-history',
|
||||
label: this.t('harvester.action.createSchedule')
|
||||
},
|
||||
{
|
||||
action: 'restoreVM',
|
||||
enabled: !!this.actions?.restore,
|
||||
@ -323,6 +329,16 @@ export default class VirtVm extends HarvesterResource {
|
||||
);
|
||||
}
|
||||
|
||||
createSchedule(resources = this) {
|
||||
const router = this.currentRouter();
|
||||
|
||||
router.push({
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||
params: { resource: HCI.SCHEDULE_VM_BACKUP },
|
||||
query: { vmNamespace: this.metadata.namespace, vmName: this.metadata.name }
|
||||
});
|
||||
}
|
||||
|
||||
backupVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
|
||||
@ -11,6 +11,7 @@ export const HCI = {
|
||||
SETTING: 'harvesterhci.io.setting',
|
||||
UPGRADE: 'harvesterhci.io.upgrade',
|
||||
UPGRADE_LOG: 'harvesterhci.io.upgradelog',
|
||||
SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup',
|
||||
BACKUP: 'harvesterhci.io.virtualmachinebackup',
|
||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||
|
||||
11
pkg/harvester/utils/cron.js
Normal file
11
pkg/harvester/utils/cron.js
Normal file
@ -0,0 +1,11 @@
|
||||
import cronstrue from 'cronstrue';
|
||||
|
||||
export function isCronValid(schedule = '') {
|
||||
try {
|
||||
const hint = cronstrue.toString(schedule);
|
||||
|
||||
return !!hint;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -5960,10 +5960,10 @@ cron-validator@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.2.0.tgz#952d2c926b85724dfe9c0d0ca781fe956124de93"
|
||||
integrity sha512-fX9eq71ToAt4bJeJzFNe8OCljKNQdc2Otw4kZDfB3vyplrAyEO9Q20YgmCJ4pr+jI/QQ2yizM87Eh+b2Ty7GuQ==
|
||||
|
||||
cronstrue@1.95.0:
|
||||
version "1.95.0"
|
||||
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.95.0.tgz#171df1fad8b0f0cb636354dd1d7842161c15478f"
|
||||
integrity sha512-CdbQ17Z8Na2IdrK1SiD3zmXfE66KerQZ8/iApkGsxjmUVGJPS9M9oK4FZC3LM6ohUjjq3UeaSk+90Cf3QbXDfw==
|
||||
cronstrue@1.95.0, cronstrue@2.50.0:
|
||||
version "2.50.0"
|
||||
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573"
|
||||
integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==
|
||||
|
||||
cross-env@6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user