harvester-ui-extension/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue
Andy Lee 4aabf0b7a3
fix: hide VM take backup action if backup target is not available (#512)
* fix: hide VM take backup action if backup target is not available

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: use extracted func

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2025-09-09 14:36:23 +08:00

269 lines
7.3 KiB
Vue

<script>
import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink';
import Masthead from '@shell/components/ResourceList/Masthead';
import ResourceTable from '@shell/components/ResourceTable';
import { isBackupTargetSettingEmpty } from '../utils/setting';
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, 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 }),
backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }),
scheduleList: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SCHEDULE_VM_BACKUP }),
});
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 = isBackupTargetSettingEmpty(backupTargetResource);
if (backupTargetResource && !isEmpty) {
this.testConnect();
}
}
},
data() {
const params = { ...this.$route.params };
const resource = params.resource;
return {
rows: [],
backups: [],
settings: [],
resource,
to: `${ HCI.SETTING }/backup-target?mode=edit`,
searchSchedule: ''
};
},
methods: {
async testConnect() {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/backuptarget/healthz');
await this.$store.dispatch('harvester/request', { url });
} catch (err) {
if (err?._status === 400 || err?._status === 503) {
this.$store.dispatch('growl/error', {
title: this.t('harvester.notification.title.error'),
message: err.errors[0]
}, { root: true });
}
}
},
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: {
headers() {
const cols = [
STATE,
NAME,
NAMESPACE,
{
name: 'targetVM',
labelKey: 'harvester.tableHeaders.targetVm',
value: 'attachVM',
align: 'left',
formatter: 'AttachVMWithName'
},
];
if (this.schedulingVMBackupFeatureEnabled) {
cols.push({
name: 'backupCreatedFrom',
labelKey: 'harvester.tableHeaders.vmSchedule',
value: 'sourceSchedule',
formatter: 'BackupCreatedFrom',
});
}
cols.push(...[
{
name: 'backupTarget',
labelKey: 'tableHeaders.backupTarget',
value: 'backupTarget',
align: 'left',
formatter: 'HarvesterBackupTargetValidation'
},
{
name: 'readyToUse',
labelKey: 'tableHeaders.readyToUse',
value: 'status.readyToUse',
align: 'center',
formatter: 'Checked',
},
]);
if (this.hasBackupProgresses) {
cols.push({
name: 'backupProgress',
labelKey: 'tableHeaders.progress',
value: 'backupProgress',
align: 'left',
formatter: 'HarvesterBackupProgressBar',
});
}
cols.push(AGE);
return cols;
},
schedulingVMBackupFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
},
hasBackupProgresses() {
return !!this.backups.find((r) => r.status?.progress !== undefined);
},
filteredRows() {
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');
},
isEmptyValue() {
return isBackupTargetSettingEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
},
errorMessage() {
return this.backupTargetResource?.errMessage;
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Masthead
:schema="schema"
:resource="resource"
:create-button-label="t('harvester.backup.createText')"
/>
<Banner
v-if="(errorMessage || isEmptyValue) && canUpdate"
color="error"
>
<MessageLink
v-if="isEmptyValue"
:to="to"
prefix-label="harvester.backup.message.noSetting.prefix"
middle-label="harvester.backup.message.noSetting.middle"
suffix-label="harvester.backup.message.noSetting.suffix"
/>
<MessageLink
v-else
:to="to"
prefix-label="harvester.backup.message.errorTip.prefix"
middle-label="harvester.backup.message.errorTip.middle"
>
<template #suffix>
{{ t('harvester.backup.message.errorTip.suffix') }} {{ errorMessage }}
</template>
</MessageLink>
</Banner>
<div v-else-if="canUpdate">
<Banner
color="info"
>
<MessageLink
:to="to"
prefix-label="harvester.backup.message.viewSetting.prefix"
middle-label="harvester.backup.message.viewSetting.middle"
suffix-label="harvester.backup.message.viewSetting.suffix"
/>
</Banner>
</div>
<ResourceTable
v-bind="$attrs"
:headers="headers"
:groupable="true"
:rows="filteredRows"
:sort-generation-fn="sortGenerationFn"
:schema="schema"
key-field="_key"
default-sort-by="age"
>
<template
v-if="schedulingVMBackupFeatureEnabled"
#more-header-middle
>
<FilterVMSchedule
:rows="getRawRows"
@change-rows="changeRows"
/>
</template>
<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>