feat: add restore by backup record supports (#4511)

#### What type of PR is this?

/area console
/kind feature
/milestone 2.9.x

#### What this PR does / why we need it:

支持选择已有备份进行恢复。

<img width="1628" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/5e3169f7-a604-4e20-9c4e-f6bc68c58d59">

#### Special notes for your reviewer:

需要测试在恢复界面通过选择已有备份进行恢复的功能是否正常。

#### Does this PR introduce a user-facing change?

```release-note
系统恢复功能支持选择已有备份进行系统恢复。
```
pull/4533/head
Ryan Wang 2023-09-01 11:00:19 +08:00 committed by GitHub
parent 799a897622
commit 6dd77af7f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 47 deletions

View File

@ -1003,6 +1003,9 @@ core:
toast_success: Requested to restart
remote_download:
button: Download and restore
restore_by_backup:
button: Restore
title: Are you sure you want to restore from this backup?
list:
phases:
pending: Pending
@ -1025,6 +1028,8 @@ core:
label: Remote
fields:
url: Remote URL
backup:
label: Restore from backup
exception:
not_found:
message: Page not found

View File

@ -1003,6 +1003,9 @@ core:
toast_success: 已请求重启
remote_download:
button: 下载并恢复
restore_by_backup:
button: 恢复
title: 确认要从此备份进行恢复吗?
list:
phases:
pending: 准备中
@ -1025,6 +1028,8 @@ core:
label: 远程恢复
fields:
url: 下载地址
backup:
label: 从备份恢复
exception:
not_found:
message: 没有找到该页面

View File

@ -1003,6 +1003,9 @@ core:
toast_success: 已請求重啟
remote_download:
button: 下載並還原
restore_by_backup:
button: 還原
title: 確認要從此備份進行還原嗎?
list:
phases:
pending: 準備中
@ -1016,15 +1019,17 @@ core:
first: 1. 還原過程可能需要較長時間,期間請勿重新整理頁面。
second: 2. 在還原過程中,雖然已有的資料不會被清除,但若有衝突的資料將被覆蓋。
third: 3. 還原完成後需要重新啟動 Halo 才能正常載入系統資源。
complete: 恢復完成,等待重啟...
complete: 還原完成,等待重啟...
start: 開始還原
tabs:
local:
label: 上傳
remote:
label: 遠程恢復
label: 遠程還原
fields:
url: 下載地址
backup:
label: 從備份還原
exception:
not_found:
message: 沒有找到該頁面

View File

@ -23,9 +23,15 @@ import type { OperationItem } from "packages/shared/dist";
const queryClient = useQueryClient();
const { t } = useI18n();
const props = defineProps<{
backup: Backup;
}>();
const props = withDefaults(
defineProps<{
backup: Backup;
showOperations: boolean;
}>(),
{
showOperations: true,
}
);
const { backup } = toRefs(props);
@ -185,8 +191,9 @@ const { operationItems } = useOperationItemExtensionPoint<Backup>(
</span>
</template>
</VEntityField>
<slot name="end"></slot>
</template>
<template #dropdownItems>
<template v-if="showOperations" #dropdownItems>
<EntityDropdownItems :dropdown-items="operationItems" :item="backup" />
</template>
</VEntity>

View File

@ -1,9 +1,45 @@
import { apiClient } from "@/utils/api-client";
import { Dialog, Toast } from "@halo-dev/components";
import { useQueryClient } from "@tanstack/vue-query";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
import dayjs from "dayjs";
import { BackupStatusPhaseEnum } from "@halo-dev/api-client";
import { useI18n } from "vue-i18n";
export function useBackupFetch() {
return useQuery({
queryKey: ["backups"],
queryFn: async () => {
const { data } =
await apiClient.extension.backup.listmigrationHaloRunV1alpha1Backup({
sort: ["metadata.creationTimestamp,desc"],
});
return data;
},
refetchInterval(data) {
const deletingBackups = data?.items.filter((backup) => {
return !!backup.metadata.deletionTimestamp;
});
if (deletingBackups?.length) {
return 1000;
}
const pendingBackups = data?.items.filter((backup) => {
return (
backup.status?.phase === BackupStatusPhaseEnum.Pending ||
backup.status?.phase === BackupStatusPhaseEnum.Running
);
});
if (pendingBackups?.length) {
return 3000;
}
return false;
},
});
}
export function useBackup() {
const { t } = useI18n();
const queryClient = useQueryClient();

View File

@ -1,47 +1,9 @@
<script lang="ts" setup>
import { apiClient } from "@/utils/api-client";
import { useQuery } from "@tanstack/vue-query";
import { BackupStatusPhaseEnum } from "@halo-dev/api-client";
import { VButton, VEmpty, VLoading } from "@halo-dev/components";
import BackupListItem from "../components/BackupListItem.vue";
import { useBackupFetch } from "../composables/use-backup";
const {
data: backups,
isLoading,
isFetching,
refetch,
} = useQuery({
queryKey: ["backups"],
queryFn: async () => {
const { data } =
await apiClient.extension.backup.listmigrationHaloRunV1alpha1Backup({
sort: ["metadata.creationTimestamp,desc"],
});
return data;
},
refetchInterval(data) {
const deletingBackups = data?.items.filter((backup) => {
return !!backup.metadata.deletionTimestamp;
});
if (deletingBackups?.length) {
return 1000;
}
const pendingBackups = data?.items.filter((backup) => {
return (
backup.status?.phase === BackupStatusPhaseEnum.Pending ||
backup.status?.phase === BackupStatusPhaseEnum.Running
);
});
if (pendingBackups?.length) {
return 3000;
}
return false;
},
});
const { data: backups, isLoading, isFetching, refetch } = useBackupFetch();
</script>
<template>

View File

@ -6,6 +6,7 @@ import {
Toast,
VAlert,
VButton,
VEntityField,
VLoading,
VTabItem,
VTabs,
@ -15,8 +16,18 @@ import axios from "axios";
import { computed } from "vue";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useBackupFetch } from "../composables/use-backup";
import BackupListItem from "../components/BackupListItem.vue";
import type { Backup } from "packages/api-client/dist";
const { t } = useI18n();
const { data: backups } = useBackupFetch();
const normalBackups = computed(() => {
return backups.value?.items.filter((item) => {
return item.status?.phase === "SUCCEEDED";
});
});
const complete = ref(false);
const showUploader = ref(false);
@ -58,6 +69,22 @@ const { isLoading: downloading, mutate: handleRemoteDownload } = useMutation({
},
});
function handleRestoreFromBackup(backup: Backup) {
Dialog.info({
title: t("core.backup.operations.restore_by_backup.title"),
confirmText: t("core.common.buttons.confirm"),
showCancel: false,
async onConfirm() {
await apiClient.migration.restoreBackup({
backupName: backup.metadata.name,
});
setTimeout(() => {
onProcessCompleted();
}, 200);
},
});
}
useQuery({
queryKey: ["check-health"],
queryFn: async () => {
@ -142,6 +169,34 @@ useQuery({
</VButton>
</div>
</VTabItem>
<VTabItem
id="backups"
:label="$t('core.backup.restore.tabs.backup.label')"
>
<ul
class="box-border h-full w-full divide-y divide-gray-100 overflow-hidden rounded-base"
role="list"
>
<li v-for="(backup, index) in normalBackups" :key="index">
<BackupListItem :show-operations="false" :backup="backup">
<template #end>
<VEntityField v-permission="['system:themes:manage']">
<template #description>
<VButton
size="sm"
@click="handleRestoreFromBackup(backup)"
>
{{
$t("core.backup.operations.restore_by_backup.button")
}}
</VButton>
</template>
</VEntityField>
</template>
</BackupListItem>
</li>
</ul>
</VTabItem>
</VTabs>
</div>
</div>