+import { computed, reactive, ref, watchEffect } from 'vue'
+import useDialog from 'stores/dialog'
+import { useI18n } from 'vue-i18n'
+import useBrowserStore from 'stores/browser.js'
+import FileSaveInput from '@/components/common/FileSaveInput.vue'
+import { isEmpty, map, size } from 'lodash'
+import { decodeRedisKey } from '@/utils/key_convert.js'
+import dayjs from 'dayjs'
+
+const exportKeyForm = reactive({
+ server: '',
+ db: 0,
+ keys: [],
+ file: '',
+})
+
+const dialogStore = useDialog()
+const browserStore = useBrowserStore()
+const loading = ref(false)
+const deleting = ref(false)
+watchEffect(() => {
+ if (dialogStore.exportKeyDialogVisible) {
+ const { server, db, keys } = dialogStore.exportKeyParam
+ exportKeyForm.server = server
+ exportKeyForm.db = db
+ exportKeyForm.keys = keys
+ // exportKeyForm.async = true
+ deleting.value = false
+ }
+})
+
+const keyLines = computed(() => {
+ return map(exportKeyForm.keys, (k) => decodeRedisKey(k))
+})
+
+const exportEnable = computed(() => {
+ return !isEmpty(exportKeyForm.keys) && !isEmpty(exportKeyForm.file)
+})
+
+const i18n = useI18n()
+const onConfirmDelete = async () => {
+ try {
+ deleting.value = true
+ const { server, db, keys, file } = exportKeyForm
+ browserStore.exportKeys(server, db, keys, file).catch((e) => {})
+ } catch (e) {
+ $message.error(e.message)
+ return
+ } finally {
+ deleting.value = false
+ }
+ dialogStore.closeExportKeyDialog()
+}
+
+const onClose = () => {
+ dialogStore.closeExportKeyDialog()
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('common.cancel') }}
+
+ {{ $t('dialogue.export.export') }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/icons/Export.vue b/frontend/src/components/icons/Export.vue
new file mode 100644
index 0000000..9d35eba
--- /dev/null
+++ b/frontend/src/components/icons/Export.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/icons/Import.vue b/frontend/src/components/icons/Import.vue
new file mode 100644
index 0000000..bac6d7a
--- /dev/null
+++ b/frontend/src/components/icons/Import.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/sidebar/BrowserPane.vue b/frontend/src/components/sidebar/BrowserPane.vue
index 3cb79e0..593cbc1 100644
--- a/frontend/src/components/sidebar/BrowserPane.vue
+++ b/frontend/src/components/sidebar/BrowserPane.vue
@@ -23,6 +23,7 @@ import useConnectionStore from 'stores/connections.js'
import ListCheckbox from '@/components/icons/ListCheckbox.vue'
import Close from '@/components/icons/Close.vue'
import More from '@/components/icons/More.vue'
+import Export from '@/components/icons/Export.vue'
const props = defineProps({
server: String,
@@ -146,6 +147,10 @@ const onDeleteChecked = () => {
browserTreeRef.value?.deleteCheckedItems()
}
+const onExportChecked = () => {
+ browserTreeRef.value?.exportCheckedItems()
+}
+
const onFlush = () => {
dialogStore.openFlushDBDialog(props.server, props.db)
}
@@ -329,6 +334,14 @@ onMounted(() => onReload())
+
{
+ const checkedKeys = tabStore.currentCheckedKeys
+ const redisKeys = map(checkedKeys, 'redisKey')
+ if (!isEmpty(redisKeys)) {
+ dialogStore.openExportKeyDialog(props.server, props.db, redisKeys)
+ }
+ },
})
diff --git a/frontend/src/langs/en-us.json b/frontend/src/langs/en-us.json
index 57734e9..5fe356b 100644
--- a/frontend/src/langs/en-us.json
+++ b/frontend/src/langs/en-us.json
@@ -79,6 +79,7 @@
"check_mode": "Check Mode",
"quit_check_mode": "Quit Check Mode",
"delete_checked": "Delete Checked Items",
+ "export_checked": "Export Checked Items",
"copy_value": "Copy Value",
"edit_value": "Edit Value",
"save_update": "Save Update",
@@ -146,6 +147,7 @@
"delete_completed": "Deletion process has been completed, {success} successed, {fail} failed",
"rename_binary_key_fail": "Rename binary key name is unsupported",
"handle_succ": "Success!",
+ "handle_cancel": "The operation has been canceled.",
"reload_succ": "Reloaded!",
"field_required": "This item should not be blank",
"spec_field_required": "\"{key}\" should not be blank",
@@ -269,6 +271,14 @@
"filter_pattern": "Pattern",
"filter_pattern_tip": "* : Matches zero or more characters. For example, 'key*' matches all keys starting with 'key'.\n? : Matches a single character. For example, 'key?' matches 'key1', 'key2'.\n[] : Matches a single character within the specified range. For example, 'key[1-3]' matches keys like 'key1', 'key2', 'key3'.\n\\ : Escape character. To match *, ?, [, or ], use the backslash '\\' for escaping."
},
+ "export": {
+ "name": "Export Keys",
+ "export": "Export",
+ "save_file": "Export Path",
+ "save_file_tip": "Select the export file save path",
+ "exporting": "Exporting key: {key} ({index}/{count})",
+ "export_completed": "Export process has been completed, {success} successed, {fail} failed"
+ },
"ttl": {
"title": "Set Key TTL"
},
diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json
index f594878..9811384 100644
--- a/frontend/src/langs/zh-cn.json
+++ b/frontend/src/langs/zh-cn.json
@@ -78,7 +78,8 @@
"flush_db": "清空数据库",
"check_mode": "勾选模式",
"quit_check_mode": "退出勾选模式",
- "delete_checked": "删除勾选项",
+ "delete_checked": "删除所选项",
+ "export_checked": "导出所选项",
"copy_value": "复制值",
"edit_value": "修改值",
"save_update": "保存修改",
@@ -146,6 +147,7 @@
"delete_completed": "已完成删除操作,成功{success}个,失败{fail}个",
"rename_binary_key_fail": "不支持重命名二进制键名",
"handle_succ": "操作成功",
+ "handle_cancel": "操作已取消",
"reload_succ": "已重新载入",
"field_required": "此项不能为空",
"spec_field_required": "{key} 不能为空",
@@ -268,6 +270,14 @@
"filter_pattern": "过滤表达式",
"filter_pattern_tip": "*:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义"
},
+ "export": {
+ "name": "导出键",
+ "export": "确认导出",
+ "save_file": "导出路径",
+ "save_file_tip": "选择保存文件路径",
+ "exporting": "正在导出键:{key} ({index}/{count})",
+ "export_completed": "已完成导出操作,成功{success}个,失败{fail}个"
+ },
"ttl": {
"title": "设置键存活时间"
},
diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js
index ed57691..b34662f 100644
--- a/frontend/src/stores/browser.js
+++ b/frontend/src/stores/browser.js
@@ -26,6 +26,7 @@ import {
ConvertValue,
DeleteKey,
DeleteOneKey,
+ ExportKey,
FlushDB,
GetCmdHistory,
GetKeyDetail,
@@ -56,6 +57,7 @@ import useConnectionStore from 'stores/connections.js'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { isRedisGlob } from '@/utils/glob_pattern.js'
import { i18nGlobal } from '@/utils/i18n.js'
+import { EventsEmit, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
const useBrowserStore = defineStore('browser', {
/**
@@ -2012,6 +2014,60 @@ const useBrowserStore = defineStore('browser', {
}
},
+ /**
+ * export multiple keys
+ * @param {string} server
+ * @param {number} db
+ * @param {string[]|number[][]} keys
+ * @param {string} path
+ * @returns {Promise}
+ */
+ async exportKeys(server, db, keys, path) {
+ const delMsgRef = $message.loading('', { duration: 0, closable: true })
+ let exported = 0
+ let failCount = 0
+ let canceled = false
+ const eventName = 'exporting:' + path
+ try {
+ EventsOn(eventName, ({ total, progress, processing }) => {
+ // update export progress
+ delMsgRef.content = i18nGlobal.t('dialogue.export.exporting', {
+ key: decodeRedisKey(processing),
+ index: progress,
+ count: total,
+ })
+ })
+ delMsgRef.onClose = () => {
+ EventsEmit('export:stop:' + path)
+ }
+ const { data, success, msg } = await ExportKey(server, db, keys, path)
+ if (success) {
+ canceled = get(data, 'canceled', false)
+ exported = get(data, 'exported', 0)
+ failCount = get(data, 'failed', 0)
+ } else {
+ $message.error(msg)
+ }
+ } finally {
+ delMsgRef.destroy()
+ EventsOff(eventName)
+ }
+ if (canceled) {
+ $message.info(i18nGlobal.t('dialogue.handle_cancel'))
+ } else if (failCount <= 0) {
+ // no fail
+ $message.success(
+ i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount }),
+ )
+ } else if (failCount >= exported) {
+ // all fail
+ $message.error(i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount }))
+ } else {
+ // some fail
+ $message.warn(i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount }))
+ }
+ },
+
/**
* flush database
* @param connName
diff --git a/frontend/src/stores/dialog.js b/frontend/src/stores/dialog.js
index 0b94e54..9bf1e0a 100644
--- a/frontend/src/stores/dialog.js
+++ b/frontend/src/stores/dialog.js
@@ -1,5 +1,5 @@
-import { defineStore } from "pinia";
-import useConnectionStore from "./connections.js";
+import { defineStore } from 'pinia'
+import useConnectionStore from './connections.js'
/**
* connection dialog type
@@ -63,6 +63,13 @@ const useDialogStore = defineStore('dialog', {
},
deleteKeyDialogVisible: false,
+ exportKeyParam: {
+ server: '',
+ db: 0,
+ keys: [],
+ },
+ exportKeyDialogVisible: false,
+
flushDBParam: {
server: '',
db: 0,
@@ -164,7 +171,7 @@ const useDialogStore = defineStore('dialog', {
*
* @param {string} server
* @param {number} db
- * @param {string | string[]} key
+ * @param {string|string[]} key
*/
openDeleteKeyDialog(server, db, key) {
this.deleteKeyParam.server = server
@@ -176,6 +183,22 @@ const useDialogStore = defineStore('dialog', {
this.deleteKeyDialogVisible = false
},
+ /**
+ *
+ * @param {string} server
+ * @param {number} db
+ * @param {string|string[]} keys
+ */
+ openExportKeyDialog(server, db, keys) {
+ this.exportKeyParam.server = server
+ this.exportKeyParam.db = db
+ this.exportKeyParam.keys = keys
+ this.exportKeyDialogVisible = true
+ },
+ closeExportKeyDialog() {
+ this.exportKeyDialogVisible = false
+ },
+
openFlushDBDialog(server, db) {
this.flushDBParam.server = server
this.flushDBParam.db = db