From b445b505be6c13fade8a3e1e8c895f95ed2658db Mon Sep 17 00:00:00 2001
From: Takagi <1103069291@qq.com>
Date: Wed, 19 Jun 2024 18:55:00 +0800
Subject: [PATCH] feat: add import and export theme configuration (#6071)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#### What type of PR is this?
/kind feature
/area ui
/milestone 2.17.x
#### What this PR does / why we need it:
为主题增加导入及导出配置的功能。
#### How to test it?
测试主题导入及导出功能是否正常可用。
测试当前主题下的不同版本导入导出后数据是否正常。
测试在当前主题下使用其他主题导出的配置是否会报错。
#### Which issue(s) this PR fixes:
Fixes #1073
#### Does this PR introduce a user-facing change?
```release-note
为单个主题配置增加导入与导出的功能。
```
---
.../modules/interface/themes/ThemeDetail.vue | 24 ++-
.../interface/themes/composables/use-theme.ts | 174 +++++++++++++++++-
ui/src/locales/en.yaml | 12 ++
ui/src/locales/zh-CN.yaml | 9 +
ui/src/locales/zh-TW.yaml | 9 +
5 files changed, 213 insertions(+), 15 deletions(-)
diff --git a/ui/console-src/modules/interface/themes/ThemeDetail.vue b/ui/console-src/modules/interface/themes/ThemeDetail.vue
index 07792ea01..9946996d8 100644
--- a/ui/console-src/modules/interface/themes/ThemeDetail.vue
+++ b/ui/console-src/modules/interface/themes/ThemeDetail.vue
@@ -1,11 +1,6 @@
@@ -126,6 +124,12 @@ const handleReloadTheme = async () => {
{{ $t("core.common.buttons.upgrade") }}
+
+ {{ $t("core.theme.operations.export_configuration.button") }}
+
+
+ {{ $t("core.theme.operations.import_configuration.button") }}
+
{{ $t("core.theme.operations.reload.button") }}
diff --git a/ui/console-src/modules/interface/themes/composables/use-theme.ts b/ui/console-src/modules/interface/themes/composables/use-theme.ts
index 416fe61a4..ebfc58e9f 100644
--- a/ui/console-src/modules/interface/themes/composables/use-theme.ts
+++ b/ui/console-src/modules/interface/themes/composables/use-theme.ts
@@ -1,10 +1,11 @@
+import { apiClient } from "@/utils/api-client";
+import { useThemeStore } from "@console/stores/theme";
+import type { Theme } from "@halo-dev/api-client";
+import { Dialog, Toast } from "@halo-dev/components";
+import { useFileDialog } from "@vueuse/core";
+import { storeToRefs } from "pinia";
import type { ComputedRef, Ref } from "vue";
import { computed, ref } from "vue";
-import type { Theme } from "@halo-dev/api-client";
-import { apiClient } from "@/utils/api-client";
-import { Dialog, Toast } from "@halo-dev/components";
-import { useThemeStore } from "@console/stores/theme";
-import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n";
interface useThemeLifeCycleReturn {
@@ -138,3 +139,166 @@ export function useThemeCustomTemplates(type: "post" | "page" | "category") {
templates,
};
}
+
+interface ExportData {
+ themeName: string;
+ version: string;
+ settingName: string;
+ configMapName: string;
+ configs: { [key: string]: string };
+}
+
+export function useThemeConfigFile(theme: Ref) {
+ const { t } = useI18n();
+
+ const handleExportThemeConfiguration = async () => {
+ if (!theme.value) {
+ console.error("No selected or activated theme");
+ return;
+ }
+
+ const { data } = await apiClient.theme.fetchThemeConfig({
+ name: theme?.value?.metadata.name as string,
+ });
+ if (!data) {
+ console.error("Failed to fetch theme config");
+ return;
+ }
+
+ const themeName = theme.value.metadata.name;
+ const exportData = {
+ themeName: themeName,
+ version: theme.value.spec.version,
+ settingName: theme.value.spec.settingName,
+ configMapName: theme.value.spec.configMapName,
+ configs: data.data,
+ } as ExportData;
+ const exportStr = JSON.stringify(exportData, null, 2);
+ const blob = new Blob([exportStr], { type: "application/json" });
+ const temporaryExportUrl = URL.createObjectURL(blob);
+ const temporaryLinkTag = document.createElement("a");
+
+ temporaryLinkTag.href = temporaryExportUrl;
+ temporaryLinkTag.download = `export-${themeName}-config-${Date.now().toString()}.json`;
+
+ document.body.appendChild(temporaryLinkTag);
+ temporaryLinkTag.click();
+
+ document.body.removeChild(temporaryLinkTag);
+ URL.revokeObjectURL(temporaryExportUrl);
+ };
+
+ const {
+ open: openSelectImportFileDialog,
+ onChange: handleImportThemeConfiguration,
+ } = useFileDialog({
+ accept: "application/json",
+ multiple: false,
+ directory: false,
+ reset: true,
+ });
+
+ handleImportThemeConfiguration(async (files) => {
+ if (files === null || files.length === 0) {
+ return;
+ }
+ const configText = await files[0].text();
+ const configJson = JSON.parse(configText || "{}");
+ if (!configJson.configs) {
+ return;
+ }
+ if (!configJson.themeName || !configJson.version) {
+ Toast.error(
+ t("core.theme.operations.import_configuration.invalid_format")
+ );
+ return;
+ }
+ if (!theme.value) {
+ console.error("No selected or activated theme");
+ return;
+ }
+ if (configJson.themeName !== theme.value.metadata.name) {
+ Toast.error(
+ t("core.theme.operations.import_configuration.mismatched_theme")
+ );
+ return;
+ }
+
+ if (configJson.version !== theme.value.spec.version) {
+ Dialog.warning({
+ title: t(
+ "core.theme.operations.import_configuration.version_mismatch.title"
+ ),
+ description: t(
+ "core.theme.operations.import_configuration.version_mismatch.description"
+ ),
+ confirmText: t("core.common.buttons.confirm"),
+ cancelText: t("core.common.buttons.cancel"),
+ onConfirm: () => {
+ handleSaveConfigMap(configJson.configs);
+ },
+ onCancel() {
+ return;
+ },
+ });
+ return;
+ }
+ handleSaveConfigMap(configJson.configs);
+ });
+
+ const handleSaveConfigMap = async (importData: Record) => {
+ if (!theme.value) {
+ return;
+ }
+ const { data } = await apiClient.theme.fetchThemeConfig({
+ name: theme.value.metadata.name as string,
+ });
+ if (!data || !data.data) {
+ return;
+ }
+ const combinedConfigData = combinedConfigMap(data.data, importData);
+ await apiClient.theme.updateThemeConfig({
+ name: theme.value.metadata.name,
+ configMap: {
+ ...data,
+ data: combinedConfigData,
+ },
+ });
+ Toast.success(t("core.common.toast.save_success"));
+ };
+
+ /**
+ * combined benchmark configuration and import configuration
+ *
+ * benchmark: { a: "{\"a\": 1}", b: "{\"b\": 2}" }
+ * expand: { a: "{\"c\": 3}", b: "{\"d\": 4}" }
+ * => { a: "{\"a\": 1, \"c\": 3}", b: "{\"b\": 2, \"d\": 4}" }
+ *
+ * benchmark: { a: "{\"a\": 1}", b: "{\"b\": 2}", d: "{\"d\": 4}"
+ * expand: { a: "{\"a\": 2}", b: "{\"b\": 3, \"d\": 4}", c: "{\"c\": 5}" }
+ * => { a: "{\"a\": 2}", b: "{\"b\": 3, \"d\": 4}", d: "{\"d\": 4}" }
+ *
+ */
+ const combinedConfigMap = (
+ benchmarkConfigMap: { [key: string]: string },
+ importConfigMap: { [key: string]: string }
+ ): { [key: string]: string } => {
+ const result = benchmarkConfigMap;
+
+ for (const key in result) {
+ const benchmarkValueJson = JSON.parse(benchmarkConfigMap[key] || "{}");
+ const expandValueJson = JSON.parse(importConfigMap[key] || "{}");
+ const combinedValue = {
+ ...benchmarkValueJson,
+ ...expandValueJson,
+ };
+ result[key] = JSON.stringify(combinedValue);
+ }
+ return result;
+ };
+
+ return {
+ handleExportThemeConfiguration,
+ openSelectImportFileDialog,
+ };
+}
diff --git a/ui/src/locales/en.yaml b/ui/src/locales/en.yaml
index 71b5f98d8..0d8cf4c87 100644
--- a/ui/src/locales/en.yaml
+++ b/ui/src/locales/en.yaml
@@ -721,6 +721,18 @@ core:
description: >-
This feature allows you to refresh the cache to view the latest web
results after modifying template files at runtime.
+ export_configuration:
+ button: Export theme configuration
+ import_configuration:
+ button: Import theme configuration
+ version_mismatch:
+ title: Version mismatch
+ description: >-
+ The imported configuration file version does not match the
+ current theme version, which may lead to compatibility issues.
+ Do you want to continue importing?
+ invalid_format: Invalid theme configuration file
+ mismatched_theme: Configuration file does not match the selected theme
list_modal:
tabs:
installed: Installed
diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml
index 11cab9e93..3284a164e 100644
--- a/ui/src/locales/zh-CN.yaml
+++ b/ui/src/locales/zh-CN.yaml
@@ -691,6 +691,15 @@ core:
button: 清理模板缓存
title: 清除模板缓存
description: 此功能适用于在运行时修改模板文件后,刷新缓存以查看最新网页结果。
+ export_configuration:
+ button: 导出主题配置
+ import_configuration:
+ button: 导入主题配置
+ version_mismatch:
+ title: 版本不匹配
+ description: 导入的配置文件版本与当前主题版本不匹配,这可能会导致兼容性问题。是否继续导入?
+ invalid_format: 无效的主题配置文件
+ mismatched_theme: 配置文件与所选主题不匹配
list_modal:
tabs:
installed: 已安装
diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml
index c5e9c69b3..643931bb5 100644
--- a/ui/src/locales/zh-TW.yaml
+++ b/ui/src/locales/zh-TW.yaml
@@ -671,6 +671,15 @@ core:
button: 清除模板快取
title: 清除模板快取
description: 此功能適用於在運行時修改模板檔案後,刷新快取以查看最新網頁結果。
+ export_configuration:
+ button: 匯出主題配置
+ import_configuration:
+ button: 匯入主題配置
+ version_mismatch:
+ title: 版本不匹配
+ description: 匯入的配置檔版本與目前主題版本不匹配,這可能會導致相容性問題。是否繼續匯入?
+ invalid_format: 無效的主題配置文件
+ mismatched_theme: 配置文件與所選主題不匹配
list_modal:
tabs:
installed: 已安裝