mirror of https://github.com/halo-dev/halo
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
parent
799a897622
commit
6dd77af7f8
|
@ -1003,6 +1003,9 @@ core:
|
||||||
toast_success: Requested to restart
|
toast_success: Requested to restart
|
||||||
remote_download:
|
remote_download:
|
||||||
button: Download and restore
|
button: Download and restore
|
||||||
|
restore_by_backup:
|
||||||
|
button: Restore
|
||||||
|
title: Are you sure you want to restore from this backup?
|
||||||
list:
|
list:
|
||||||
phases:
|
phases:
|
||||||
pending: Pending
|
pending: Pending
|
||||||
|
@ -1025,6 +1028,8 @@ core:
|
||||||
label: Remote
|
label: Remote
|
||||||
fields:
|
fields:
|
||||||
url: Remote URL
|
url: Remote URL
|
||||||
|
backup:
|
||||||
|
label: Restore from backup
|
||||||
exception:
|
exception:
|
||||||
not_found:
|
not_found:
|
||||||
message: Page not found
|
message: Page not found
|
||||||
|
|
|
@ -1003,6 +1003,9 @@ core:
|
||||||
toast_success: 已请求重启
|
toast_success: 已请求重启
|
||||||
remote_download:
|
remote_download:
|
||||||
button: 下载并恢复
|
button: 下载并恢复
|
||||||
|
restore_by_backup:
|
||||||
|
button: 恢复
|
||||||
|
title: 确认要从此备份进行恢复吗?
|
||||||
list:
|
list:
|
||||||
phases:
|
phases:
|
||||||
pending: 准备中
|
pending: 准备中
|
||||||
|
@ -1025,6 +1028,8 @@ core:
|
||||||
label: 远程恢复
|
label: 远程恢复
|
||||||
fields:
|
fields:
|
||||||
url: 下载地址
|
url: 下载地址
|
||||||
|
backup:
|
||||||
|
label: 从备份恢复
|
||||||
exception:
|
exception:
|
||||||
not_found:
|
not_found:
|
||||||
message: 没有找到该页面
|
message: 没有找到该页面
|
||||||
|
|
|
@ -1003,6 +1003,9 @@ core:
|
||||||
toast_success: 已請求重啟
|
toast_success: 已請求重啟
|
||||||
remote_download:
|
remote_download:
|
||||||
button: 下載並還原
|
button: 下載並還原
|
||||||
|
restore_by_backup:
|
||||||
|
button: 還原
|
||||||
|
title: 確認要從此備份進行還原嗎?
|
||||||
list:
|
list:
|
||||||
phases:
|
phases:
|
||||||
pending: 準備中
|
pending: 準備中
|
||||||
|
@ -1016,15 +1019,17 @@ core:
|
||||||
first: 1. 還原過程可能需要較長時間,期間請勿重新整理頁面。
|
first: 1. 還原過程可能需要較長時間,期間請勿重新整理頁面。
|
||||||
second: 2. 在還原過程中,雖然已有的資料不會被清除,但若有衝突的資料將被覆蓋。
|
second: 2. 在還原過程中,雖然已有的資料不會被清除,但若有衝突的資料將被覆蓋。
|
||||||
third: 3. 還原完成後需要重新啟動 Halo 才能正常載入系統資源。
|
third: 3. 還原完成後需要重新啟動 Halo 才能正常載入系統資源。
|
||||||
complete: 恢復完成,等待重啟...
|
complete: 還原完成,等待重啟...
|
||||||
start: 開始還原
|
start: 開始還原
|
||||||
tabs:
|
tabs:
|
||||||
local:
|
local:
|
||||||
label: 上傳
|
label: 上傳
|
||||||
remote:
|
remote:
|
||||||
label: 遠程恢復
|
label: 遠程還原
|
||||||
fields:
|
fields:
|
||||||
url: 下載地址
|
url: 下載地址
|
||||||
|
backup:
|
||||||
|
label: 從備份還原
|
||||||
exception:
|
exception:
|
||||||
not_found:
|
not_found:
|
||||||
message: 沒有找到該頁面
|
message: 沒有找到該頁面
|
||||||
|
|
|
@ -23,9 +23,15 @@ import type { OperationItem } from "packages/shared/dist";
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
backup: Backup;
|
defineProps<{
|
||||||
}>();
|
backup: Backup;
|
||||||
|
showOperations: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
showOperations: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { backup } = toRefs(props);
|
const { backup } = toRefs(props);
|
||||||
|
|
||||||
|
@ -185,8 +191,9 @@ const { operationItems } = useOperationItemExtensionPoint<Backup>(
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
|
<slot name="end"></slot>
|
||||||
</template>
|
</template>
|
||||||
<template #dropdownItems>
|
<template v-if="showOperations" #dropdownItems>
|
||||||
<EntityDropdownItems :dropdown-items="operationItems" :item="backup" />
|
<EntityDropdownItems :dropdown-items="operationItems" :item="backup" />
|
||||||
</template>
|
</template>
|
||||||
</VEntity>
|
</VEntity>
|
||||||
|
|
|
@ -1,9 +1,45 @@
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { Dialog, Toast } from "@halo-dev/components";
|
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 dayjs from "dayjs";
|
||||||
|
import { BackupStatusPhaseEnum } from "@halo-dev/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
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() {
|
export function useBackup() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
|
@ -1,47 +1,9 @@
|
||||||
<script lang="ts" setup>
|
<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 { VButton, VEmpty, VLoading } from "@halo-dev/components";
|
||||||
import BackupListItem from "../components/BackupListItem.vue";
|
import BackupListItem from "../components/BackupListItem.vue";
|
||||||
|
import { useBackupFetch } from "../composables/use-backup";
|
||||||
|
|
||||||
const {
|
const { data: backups, isLoading, isFetching, refetch } = useBackupFetch();
|
||||||
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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Toast,
|
Toast,
|
||||||
VAlert,
|
VAlert,
|
||||||
VButton,
|
VButton,
|
||||||
|
VEntityField,
|
||||||
VLoading,
|
VLoading,
|
||||||
VTabItem,
|
VTabItem,
|
||||||
VTabs,
|
VTabs,
|
||||||
|
@ -15,8 +16,18 @@ import axios from "axios";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
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 { 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 complete = ref(false);
|
||||||
const showUploader = 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({
|
useQuery({
|
||||||
queryKey: ["check-health"],
|
queryKey: ["check-health"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
@ -142,6 +169,34 @@ useQuery({
|
||||||
</VButton>
|
</VButton>
|
||||||
</div>
|
</div>
|
||||||
</VTabItem>
|
</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>
|
</VTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue