feat: support import keys from csv file

This commit is contained in:
Lykin 2023-12-27 15:44:08 +08:00
parent 2bc7a57773
commit 3fe8767c44
15 changed files with 411 additions and 73 deletions

View File

@ -1999,7 +1999,7 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
var deletedKeys = make([]any, 0, total)
var mutex sync.Mutex
del := func(ctx context.Context, cli redis.UniversalClient) error {
startTime := time.Now()
startTime := time.Now().Add(-10 * time.Second)
for i, k := range ks {
// emit progress per second
param := map[string]any{
@ -2010,6 +2010,8 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now()
runtime.EventsEmit(b.ctx, processEvent, param)
// do some sleep to prevent blocking the Redis server
time.Sleep(10 * time.Millisecond)
}
key := strutil.DecodeRedisKey(k)
@ -2026,8 +2028,6 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
canceled = true
break
}
// do some sleep to prevent blocking the Redis server
time.Sleep(100 * time.Microsecond)
}
return nil
}
@ -2093,13 +2093,17 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string)
total := len(ks)
var exported, failed int64
var canceled bool
startTime := time.Now().Add(-10 * time.Second)
for i, k := range ks {
param := map[string]any{
"total": total,
"progress": i + 1,
"processing": k,
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now()
param := map[string]any{
"total": total,
"progress": i + 1,
"processing": k,
}
runtime.EventsEmit(b.ctx, processEvent, param)
}
runtime.EventsEmit(b.ctx, processEvent, param)
key := strutil.DecodeRedisKey(k)
content, dumpErr := client.Dump(ctx, key).Bytes()
@ -2128,6 +2132,121 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string)
return
}
// ImportCSV import data from csv file
func (b *browserService) ImportCSV(server string, db int, path string, conflict int) (resp types.JSResp) {
// connect a new connection to export keys
conf := Connection().getConnection(server)
if conf == nil {
resp.Msg = fmt.Sprintf("no connection profile named: %s", server)
return
}
var client redis.UniversalClient
var err error
var connConfig = conf.ConnectionConfig
connConfig.LastDB = db
if client, err = b.createRedisClient(connConfig); err != nil {
resp.Msg = err.Error()
return
}
ctx, cancelFunc := context.WithCancel(b.ctx)
defer client.Close()
defer cancelFunc()
file, err := os.Open(path)
if err != nil {
resp.Msg = err.Error()
return
}
defer file.Close()
reader := csv.NewReader(file)
cancelEvent := "import:stop:" + path
runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
cancelFunc()
})
processEvent := "importing:" + path
var line []string
var readErr error
var key, value []byte
var ttl int64
var imported, ignored int64
var canceled bool
startTime := time.Now().Add(-10 * time.Second)
for {
readErr = nil
ttl = redis.KeepTTL
line, readErr = reader.Read()
if readErr != nil {
break
}
if len(line) < 1 {
continue
}
if key, readErr = hex.DecodeString(line[0]); readErr != nil {
continue
}
if value, readErr = hex.DecodeString(line[1]); readErr != nil {
continue
}
// get ttl
if len(line) > 2 {
if ttl, readErr = strconv.ParseInt(line[2], 10, 64); readErr != nil {
ttl = redis.KeepTTL
}
}
if conflict == 0 {
readErr = client.RestoreReplace(ctx, string(key), time.Duration(ttl), string(value)).Err()
} else {
keyStr := string(key)
// go-redis may crash when batch calling restore
// use "exists" to filter first
if n, _ := client.Exists(ctx, keyStr).Result(); n <= 0 {
readErr = client.Restore(ctx, keyStr, time.Duration(ttl), string(value)).Err()
} else {
readErr = errors.New("key existed")
}
}
if readErr != nil {
// restore fail
ignored += 1
} else {
imported += 1
}
if errors.Is(readErr, context.Canceled) || canceled {
canceled = true
break
}
if time.Now().Sub(startTime).Milliseconds() > 100 {
startTime = time.Now()
param := map[string]any{
"imported": imported,
"ignored": ignored,
//"processing": string(key),
}
runtime.EventsEmit(b.ctx, processEvent, param)
// do some sleep to prevent blocking the Redis server
time.Sleep(10 * time.Millisecond)
}
}
runtime.EventsOff(ctx, cancelEvent)
resp.Success = true
resp.Data = struct {
Canceled bool `json:"canceled"`
Imported int64 `json:"imported"`
Ignored int64 `json:"ignored"`
}{
Canceled: canceled,
Imported: imported,
Ignored: ignored,
}
return
}
// FlushDB flush database
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
item, err := b.getRedisClient(connName, db)

View File

@ -44,10 +44,16 @@ func (s *systemService) Start(ctx context.Context) {
}
// SelectFile open file dialog to select a file
func (s *systemService) SelectFile(title string) (resp types.JSResp) {
func (s *systemService) SelectFile(title string, extensions []string) (resp types.JSResp) {
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
return runtime.FileFilter{
Pattern: "*." + extensions[i],
}
})
filepath, err := runtime.OpenFileDialog(s.ctx, runtime.OpenDialogOptions{
Title: title,
ShowHiddenFiles: true,
Filters: filters,
})
if err != nil {
resp.Msg = err.Error()

View File

@ -19,6 +19,7 @@ import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
import AboutDialog from '@/components/dialogs/AboutDialog.vue'
import FlushDbDialog from '@/components/dialogs/FlushDbDialog.vue'
import ExportKeyDialog from '@/components/dialogs/ExportKeyDialog.vue'
import ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue'
const prefStore = usePreferencesStore()
const connectionStore = useConnectionStore()
@ -69,6 +70,7 @@ watch(
<rename-key-dialog />
<delete-key-dialog />
<export-key-dialog />
<import-key-dialog />
<flush-db-dialog />
<set-ttl-dialog />
<preferences-dialog />

View File

@ -1,17 +1,18 @@
<script setup>
import { SelectFile } from 'wailsjs/go/services/systemService.js'
import { get } from 'lodash'
import { get, isEmpty } from 'lodash'
const props = defineProps({
value: String,
placeholder: String,
disabled: Boolean,
ext: String,
})
const emit = defineEmits(['update:value'])
const handleSelectFile = async () => {
const { success, data } = await SelectFile()
const { success, data } = await SelectFile('', isEmpty(props.ext) ? null : [props.ext])
if (success) {
const path = get(data, 'path', '')
emit('update:value', path)

View File

@ -18,15 +18,14 @@ const exportKeyForm = reactive({
const dialogStore = useDialog()
const browserStore = useBrowserStore()
const loading = ref(false)
const deleting = ref(false)
const exporting = 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
exporting.value = false
}
})
@ -39,16 +38,16 @@ const exportEnable = computed(() => {
})
const i18n = useI18n()
const onConfirmDelete = async () => {
const onConfirmExport = async () => {
try {
deleting.value = true
exporting.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
exporting.value = false
}
dialogStore.closeExportKeyDialog()
}
@ -101,7 +100,7 @@ const onClose = () => {
:focusable="false"
:loading="loading"
type="error"
@click="onConfirmDelete">
@click="onConfirmExport">
{{ $t('dialogue.export.export') }}
</n-button>
</div>

View File

@ -0,0 +1,122 @@
<script setup>
import { computed, reactive, ref, watchEffect } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js'
import { isEmpty } from 'lodash'
import FileOpenInput from '@/components/common/FileOpenInput.vue'
const importKeyForm = reactive({
server: '',
db: 0,
file: '',
type: 0,
conflict: 0,
})
const dialogStore = useDialog()
const browserStore = useBrowserStore()
const loading = ref(false)
const importing = ref(false)
watchEffect(() => {
if (dialogStore.importKeyDialogVisible) {
const { server, db } = dialogStore.importKeyParam
importKeyForm.server = server
importKeyForm.db = db
importKeyForm.file = ''
importKeyForm.type = 0
importKeyForm.conflict = 0
importing.value = false
}
})
const i18n = useI18n()
const conflictOption = [
{
value: 0,
label: i18n.t('dialogue.import.conflict_overwrite'),
},
{
value: 1,
label: i18n.t('dialogue.import.conflict_ignore'),
},
]
const importEnable = computed(() => {
return !isEmpty(importKeyForm.file)
})
const onConfirmImport = async () => {
try {
importing.value = true
const { server, db, file, conflict } = importKeyForm
browserStore.importKeysFromCSVFile(server, db, file, conflict).catch((e) => {})
} catch (e) {
$message.error(e.message)
return
} finally {
importing.value = false
}
dialogStore.closeImportKeyDialog()
}
const onClose = () => {
dialogStore.closeImportKeyDialog()
}
</script>
<template>
<n-modal
v-model:show="dialogStore.importKeyDialogVisible"
:closable="false"
:close-on-esc="false"
:mask-closable="false"
:show-icon="false"
:title="$t('dialogue.import.name')"
preset="dialog"
transform-origin="center">
<n-spin :show="loading">
<n-form :model="importKeyForm" :show-require-mark="false" label-placement="top">
<n-grid :x-gap="10">
<n-form-item-gi :label="$t('dialogue.key.server')" :span="12">
<n-input :autofocus="false" :value="importKeyForm.server" readonly />
</n-form-item-gi>
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
<n-input :autofocus="false" :value="importKeyForm.db.toString()" readonly />
</n-form-item-gi>
</n-grid>
<n-form-item :label="$t('dialogue.import.open_csv_file')" required>
<file-open-input
v-model:value="importKeyForm.file"
:placeholder="$t('dialogue.import.open_csv_file_tip')"
ext="csv" />
</n-form-item>
<n-form-item :label="$t('dialogue.import.conflict_handle')">
<n-radio-group v-model:value="importKeyForm.conflict">
<n-radio-button
v-for="(op, i) in conflictOption"
:key="i"
:label="op.label"
:value="op.value" />
</n-radio-group>
</n-form-item>
</n-form>
</n-spin>
<template #action>
<div class="flex-item n-dialog__action">
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button
:disabled="!importEnable"
:focusable="false"
:loading="loading"
type="error"
@click="onConfirmImport">
{{ $t('dialogue.export.export') }}
</n-button>
</div>
</template>
</n-modal>
</template>
<style lang="scss" scoped></style>

View File

@ -9,36 +9,24 @@ const props = defineProps({
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<mask
id="icon-122098f7f10b972"
height="48"
maskUnits="userSpaceOnUse"
style="mask-type: alpha"
width="48"
x="0"
y="0">
<path d="M48 0H0V48H48V0Z" fill="currentColor" />
</mask>
<g mask="url(#icon-122098f7f10b972)">
<path
:stroke-width="props.strokeWidth"
d="M6 24.0083V42H42V24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M33 15L24 6L15 15"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M23.9917 32V6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</g>
<path
:stroke-width="props.strokeWidth"
d="M6 24.0083V42H42V24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M33 23L24 32L15 23"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M23.9917 6V32"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>

View File

@ -11,19 +11,19 @@ const props = defineProps({
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M6 24.0083V42H42V24"
d="M6 24V42H42V24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M33 23L24 32L15 23"
d="M33 15L24 6L15 15"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M23.9917 6V32"
d="M24 6V32"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />

View File

@ -45,7 +45,7 @@ const onUpdate = (val) => {
</script>
<template>
<n-form-item :label="$t('interface.type')">
<n-form-item :label="$t('dialogue.field.conflict_handle')">
<n-radio-group :value="props.type" @update:value="(val) => emit('update:type', val)">
<n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" />
</n-radio-group>

View File

@ -41,7 +41,7 @@ defineExpose({
</script>
<template>
<n-form-item :label="$t('dialogue.field.element')" required>
<n-form-item :label="$t('dialogue.field.conflict_handle')" required>
<n-dynamic-input v-model:value="zset" @create="onCreate" @update:value="onUpdate">
<template #default="{ value }">
<n-input

View File

@ -25,6 +25,7 @@ import Close from '@/components/icons/Close.vue'
import More from '@/components/icons/More.vue'
import Export from '@/components/icons/Export.vue'
import { ConnectionType } from '@/consts/connection_type.js'
import Import from '@/components/icons/Import.vue'
const props = defineProps({
server: String,
@ -64,6 +65,7 @@ const dbSelectOptions = computed(() => {
const moreOptions = computed(() => {
return [
{ key: 'import', label: i18n.t('interface.import_key'), icon: render.renderIcon(Import, { strokeWidth: 3.5 }) },
{ key: 'flush', label: i18n.t('interface.flush_db'), icon: render.renderIcon(Delete, { strokeWidth: 3.5 }) },
{
key: 'disconnect',
@ -162,6 +164,10 @@ const onExportChecked = () => {
browserTreeRef.value?.exportCheckedItems()
}
const onImportData = () => {
dialogStore.openImportKeyDialog(props.server, props.db)
}
const onFlush = () => {
dialogStore.openFlushDBDialog(props.server, props.db)
}
@ -215,6 +221,9 @@ const onMatchInput = (matchVal, filterVal) => {
const onSelectOptions = (select) => {
switch (select) {
case 'import':
onImportData()
break
case 'flush':
onFlush()
break

View File

@ -75,6 +75,7 @@
"rename_key": "Rename Key",
"delete_key": "Delete Key",
"batch_delete_key": "Batch Delete Keys",
"import_key": "Import Key",
"flush_db": "Flush Database",
"check_mode": "Check Mode",
"quit_check_mode": "Quit Check Mode",
@ -225,8 +226,7 @@
},
"cluster": {
"title": "Cluster",
"enable": "Serve as Cluster Node",
"readonly": "Enables read-only commands on slave nodes"
"enable": "Serve as Cluster Node"
}
},
"group": {
@ -252,6 +252,7 @@
"field": {
"new": "Add New Field",
"new_item": "Add New Item",
"conflict_handle": "When Field Conflict",
"overwrite_field": "Overwrite Existing Field",
"ignore_field": "Ignore Existing Field",
"insert_type": "Insert",
@ -272,12 +273,23 @@
"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",
"name": "Export Data",
"export": "Export",
"save_file": "Export Path",
"save_file_tip": "Select the export file save path",
"exporting": "Exporting key({index}/{count}): {key}",
"export_completed": "Export process has been completed, {success} successed, {fail} failed"
"save_file_tip": "Select the path to save exported file",
"exporting": "Exporting keys({index}/{count})",
"export_completed": "Export completed, {success} successes, {fail} failed"
},
"import": {
"name": "Import Data",
"import": "Import",
"open_csv_file": "Import File",
"open_csv_file_tip": "Select the file for import",
"conflict_handle": "Handle Key Conflict",
"conflict_overwrite": "Overwrite",
"conflict_ignore": "Ignore",
"importing": "Importing Keys imported/overwrite:{imported} conflict/fail:{conflict}",
"import_completed": "Import completed, {success} successes, {ignored} failed"
},
"ttl": {
"title": "Set Key TTL"

View File

@ -75,6 +75,7 @@
"rename_key": "重命名键",
"delete_key": "删除键",
"batch_delete_key": "批量删除键",
"import_key": "导入数据",
"flush_db": "清空数据库",
"check_mode": "勾选模式",
"quit_check_mode": "退出勾选模式",
@ -251,8 +252,9 @@
"field": {
"new": "添加新字段",
"new_item": "添加新元素",
"overwrite_field": "覆盖同名字段",
"ignore_field": "忽略同名字段",
"conflict_handle": "字段冲突处理",
"overwrite_field": "覆盖",
"ignore_field": "忽略",
"insert_type": "插入类型",
"append_item": "尾部追加",
"prepend_item": "插入头部",
@ -271,13 +273,24 @@
"filter_pattern_tip": "*:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义"
},
"export": {
"name": "导出",
"name": "导出数据",
"export": "确认导出",
"save_file": "导出路径",
"save_file_tip": "选择保存文件路径",
"exporting": "正在导出键({index}/{count}){key}",
"save_file_tip": "选择导出文件保存路径",
"exporting": "正在导出键({index}/{count})",
"export_completed": "已完成导出操作,成功{success}个,失败{fail}个"
},
"import": {
"name": "导入数据",
"import": "确认导入",
"open_csv_file": "导入文件路径",
"open_csv_file_tip": "选择需要导入的文件",
"conflict_handle": "键冲突处理",
"conflict_overwrite": "覆盖",
"conflict_ignore": "忽略",
"importing": "正在导入数据 已导入/覆盖:{imported} 冲突/失败:{conflict}",
"import_completed": "已完成导入操作,成功{success}个,忽略{ignored}个"
},
"ttl": {
"title": "设置键存活时间"
},

View File

@ -17,6 +17,7 @@ import {
GetKeySummary,
GetKeyType,
GetSlowLogs,
ImportCSV,
LoadAllKeys,
LoadNextAllKeys,
LoadNextKeys,
@ -1527,7 +1528,7 @@ const useBrowserStore = defineStore('browser', {
* @return {Promise<void>}
*/
async deleteKeys(server, db, keys) {
const delMsgRef = $message.loading('', { duration: 0, closable: true })
const msgRef = $message.loading('', { duration: 0, closable: true })
let deleted = []
let failCount = 0
let canceled = false
@ -1542,13 +1543,13 @@ const useBrowserStore = defineStore('browser', {
maxProgress = progress
}
const k = decodeRedisKey(processing)
delMsgRef.content = i18nGlobal.t('dialogue.deleting_key', {
msgRef.content = i18nGlobal.t('dialogue.deleting_key', {
key: k,
index: maxProgress,
count: total,
})
})
delMsgRef.onClose = () => {
msgRef.onClose = () => {
EventsEmit(cancelEvent)
}
const { data, success, msg } = await DeleteKeys(server, db, keys, serialNo)
@ -1560,7 +1561,7 @@ const useBrowserStore = defineStore('browser', {
$message.error(msg)
}
} finally {
delMsgRef.destroy()
msgRef.destroy()
EventsOff(eventName)
// clear checked keys
const tab = useTabStore()
@ -1608,7 +1609,7 @@ const useBrowserStore = defineStore('browser', {
* @returns {Promise<void>}
*/
async exportKeys(server, db, keys, path) {
const delMsgRef = $message.loading('', { duration: 0, closable: true })
const msgRef = $message.loading('', { duration: 0, closable: true })
let exported = 0
let failCount = 0
let canceled = false
@ -1616,13 +1617,13 @@ const useBrowserStore = defineStore('browser', {
try {
EventsOn(eventName, ({ total, progress, processing }) => {
// update export progress
delMsgRef.content = i18nGlobal.t('dialogue.export.exporting', {
key: decodeRedisKey(processing),
msgRef.content = i18nGlobal.t('dialogue.export.exporting', {
// key: decodeRedisKey(processing),
index: progress,
count: total,
})
})
delMsgRef.onClose = () => {
msgRef.onClose = () => {
EventsEmit('export:stop:' + path)
}
const { data, success, msg } = await ExportKey(server, db, keys, path)
@ -1634,7 +1635,7 @@ const useBrowserStore = defineStore('browser', {
$message.error(msg)
}
} finally {
delMsgRef.destroy()
msgRef.destroy()
EventsOff(eventName)
}
if (canceled) {
@ -1653,6 +1654,52 @@ const useBrowserStore = defineStore('browser', {
}
},
/**
* import multiple keys from csv file
* @param {string} server
* @param {number} db
* @param {string} path
* @param {int} conflict
* @return {Promise<void>}
*/
async importKeysFromCSVFile(server, db, path, conflict) {
const msgRef = $message.loading('', { duration: 0, closable: true })
let imported = 0
let ignored = 0
let canceled = false
const eventName = 'importing:' + path
try {
EventsOn(eventName, ({ imported = 0, ignored = 0 }) => {
// update export progress
msgRef.content = i18nGlobal.t('dialogue.import.importing', {
// key: decodeRedisKey(processing),
imported,
conflict: ignored,
})
})
msgRef.onClose = () => {
EventsEmit('import:stop:' + path)
}
const { data, success, msg } = await ImportCSV(server, db, path, conflict)
if (success) {
canceled = get(data, 'canceled', false)
imported = get(data, 'imported', 0)
ignored = get(data, 'ignored', 0)
} else {
$message.error(msg)
}
} finally {
msgRef.destroy()
EventsOff(eventName)
}
if (canceled) {
$message.info(i18nGlobal.t('dialogue.handle_cancel'))
} else {
// no fail
$message.success(i18nGlobal.t('dialogue.import.import_completed', { success: imported, ignored }))
}
},
/**
* flush database
* @param server

View File

@ -70,6 +70,12 @@ const useDialogStore = defineStore('dialog', {
},
exportKeyDialogVisible: false,
importKeyParam: {
server: '',
db: 0,
},
importKeyDialogVisible: false,
flushDBParam: {
server: '',
db: 0,
@ -199,6 +205,20 @@ const useDialogStore = defineStore('dialog', {
this.exportKeyDialogVisible = false
},
/**
*
* @param {string} server
* @param {number} db
*/
openImportKeyDialog(server, db) {
this.importKeyParam.server = server
this.importKeyParam.db = db
this.importKeyDialogVisible = true
},
closeImportKeyDialog() {
this.importKeyDialogVisible = false
},
openFlushDBDialog(server, db) {
this.flushDBParam.server = server
this.flushDBParam.db = db