feat: support import keys from csv file
This commit is contained in:
parent
2bc7a57773
commit
3fe8767c44
|
@ -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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -9,17 +9,6 @@ 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"
|
||||
|
@ -28,17 +17,16 @@ const props = defineProps({
|
|||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M33 15L24 6L15 15"
|
||||
d="M33 23L24 32L15 23"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
:stroke-width="props.strokeWidth"
|
||||
d="M23.9917 32V6"
|
||||
d="M23.9917 6V32"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": "设置键存活时间"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue