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 deletedKeys = make([]any, 0, total)
|
||||||
var mutex sync.Mutex
|
var mutex sync.Mutex
|
||||||
del := func(ctx context.Context, cli redis.UniversalClient) error {
|
del := func(ctx context.Context, cli redis.UniversalClient) error {
|
||||||
startTime := time.Now()
|
startTime := time.Now().Add(-10 * time.Second)
|
||||||
for i, k := range ks {
|
for i, k := range ks {
|
||||||
// emit progress per second
|
// emit progress per second
|
||||||
param := map[string]any{
|
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 {
|
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
|
||||||
startTime = time.Now()
|
startTime = time.Now()
|
||||||
runtime.EventsEmit(b.ctx, processEvent, param)
|
runtime.EventsEmit(b.ctx, processEvent, param)
|
||||||
|
// do some sleep to prevent blocking the Redis server
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
key := strutil.DecodeRedisKey(k)
|
key := strutil.DecodeRedisKey(k)
|
||||||
|
@ -2026,8 +2028,6 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
|
||||||
canceled = true
|
canceled = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// do some sleep to prevent blocking the Redis server
|
|
||||||
time.Sleep(100 * time.Microsecond)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2093,13 +2093,17 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string)
|
||||||
total := len(ks)
|
total := len(ks)
|
||||||
var exported, failed int64
|
var exported, failed int64
|
||||||
var canceled bool
|
var canceled bool
|
||||||
|
startTime := time.Now().Add(-10 * time.Second)
|
||||||
for i, k := range ks {
|
for i, k := range ks {
|
||||||
param := map[string]any{
|
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 {
|
||||||
"total": total,
|
startTime = time.Now()
|
||||||
"progress": i + 1,
|
param := map[string]any{
|
||||||
"processing": k,
|
"total": total,
|
||||||
|
"progress": i + 1,
|
||||||
|
"processing": k,
|
||||||
|
}
|
||||||
|
runtime.EventsEmit(b.ctx, processEvent, param)
|
||||||
}
|
}
|
||||||
runtime.EventsEmit(b.ctx, processEvent, param)
|
|
||||||
|
|
||||||
key := strutil.DecodeRedisKey(k)
|
key := strutil.DecodeRedisKey(k)
|
||||||
content, dumpErr := client.Dump(ctx, key).Bytes()
|
content, dumpErr := client.Dump(ctx, key).Bytes()
|
||||||
|
@ -2128,6 +2132,121 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string)
|
||||||
return
|
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
|
// FlushDB flush database
|
||||||
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
|
func (b *browserService) FlushDB(connName string, db int, async bool) (resp types.JSResp) {
|
||||||
item, err := b.getRedisClient(connName, db)
|
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
|
// 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{
|
filepath, err := runtime.OpenFileDialog(s.ctx, runtime.OpenDialogOptions{
|
||||||
Title: title,
|
Title: title,
|
||||||
ShowHiddenFiles: true,
|
ShowHiddenFiles: true,
|
||||||
|
Filters: filters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Msg = err.Error()
|
resp.Msg = err.Error()
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
|
||||||
import AboutDialog from '@/components/dialogs/AboutDialog.vue'
|
import AboutDialog from '@/components/dialogs/AboutDialog.vue'
|
||||||
import FlushDbDialog from '@/components/dialogs/FlushDbDialog.vue'
|
import FlushDbDialog from '@/components/dialogs/FlushDbDialog.vue'
|
||||||
import ExportKeyDialog from '@/components/dialogs/ExportKeyDialog.vue'
|
import ExportKeyDialog from '@/components/dialogs/ExportKeyDialog.vue'
|
||||||
|
import ImportKeyDialog from '@/components/dialogs/ImportKeyDialog.vue'
|
||||||
|
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
const connectionStore = useConnectionStore()
|
const connectionStore = useConnectionStore()
|
||||||
|
@ -69,6 +70,7 @@ watch(
|
||||||
<rename-key-dialog />
|
<rename-key-dialog />
|
||||||
<delete-key-dialog />
|
<delete-key-dialog />
|
||||||
<export-key-dialog />
|
<export-key-dialog />
|
||||||
|
<import-key-dialog />
|
||||||
<flush-db-dialog />
|
<flush-db-dialog />
|
||||||
<set-ttl-dialog />
|
<set-ttl-dialog />
|
||||||
<preferences-dialog />
|
<preferences-dialog />
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { SelectFile } from 'wailsjs/go/services/systemService.js'
|
import { SelectFile } from 'wailsjs/go/services/systemService.js'
|
||||||
import { get } from 'lodash'
|
import { get, isEmpty } from 'lodash'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: String,
|
value: String,
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
|
ext: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:value'])
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
const handleSelectFile = async () => {
|
const handleSelectFile = async () => {
|
||||||
const { success, data } = await SelectFile()
|
const { success, data } = await SelectFile('', isEmpty(props.ext) ? null : [props.ext])
|
||||||
if (success) {
|
if (success) {
|
||||||
const path = get(data, 'path', '')
|
const path = get(data, 'path', '')
|
||||||
emit('update:value', path)
|
emit('update:value', path)
|
||||||
|
|
|
@ -18,15 +18,14 @@ const exportKeyForm = reactive({
|
||||||
const dialogStore = useDialog()
|
const dialogStore = useDialog()
|
||||||
const browserStore = useBrowserStore()
|
const browserStore = useBrowserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const deleting = ref(false)
|
const exporting = ref(false)
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (dialogStore.exportKeyDialogVisible) {
|
if (dialogStore.exportKeyDialogVisible) {
|
||||||
const { server, db, keys } = dialogStore.exportKeyParam
|
const { server, db, keys } = dialogStore.exportKeyParam
|
||||||
exportKeyForm.server = server
|
exportKeyForm.server = server
|
||||||
exportKeyForm.db = db
|
exportKeyForm.db = db
|
||||||
exportKeyForm.keys = keys
|
exportKeyForm.keys = keys
|
||||||
// exportKeyForm.async = true
|
exporting.value = false
|
||||||
deleting.value = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -39,16 +38,16 @@ const exportEnable = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const onConfirmDelete = async () => {
|
const onConfirmExport = async () => {
|
||||||
try {
|
try {
|
||||||
deleting.value = true
|
exporting.value = true
|
||||||
const { server, db, keys, file } = exportKeyForm
|
const { server, db, keys, file } = exportKeyForm
|
||||||
browserStore.exportKeys(server, db, keys, file).catch((e) => {})
|
browserStore.exportKeys(server, db, keys, file).catch((e) => {})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$message.error(e.message)
|
$message.error(e.message)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
deleting.value = false
|
exporting.value = false
|
||||||
}
|
}
|
||||||
dialogStore.closeExportKeyDialog()
|
dialogStore.closeExportKeyDialog()
|
||||||
}
|
}
|
||||||
|
@ -101,7 +100,7 @@ const onClose = () => {
|
||||||
:focusable="false"
|
:focusable="false"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
type="error"
|
type="error"
|
||||||
@click="onConfirmDelete">
|
@click="onConfirmExport">
|
||||||
{{ $t('dialogue.export.export') }}
|
{{ $t('dialogue.export.export') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</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,36 +9,24 @@ const props = defineProps({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<mask
|
<path
|
||||||
id="icon-122098f7f10b972"
|
:stroke-width="props.strokeWidth"
|
||||||
height="48"
|
d="M6 24.0083V42H42V24"
|
||||||
maskUnits="userSpaceOnUse"
|
stroke="currentColor"
|
||||||
style="mask-type: alpha"
|
stroke-linecap="round"
|
||||||
width="48"
|
stroke-linejoin="round" />
|
||||||
x="0"
|
<path
|
||||||
y="0">
|
:stroke-width="props.strokeWidth"
|
||||||
<path d="M48 0H0V48H48V0Z" fill="currentColor" />
|
d="M33 23L24 32L15 23"
|
||||||
</mask>
|
stroke="currentColor"
|
||||||
<g mask="url(#icon-122098f7f10b972)">
|
stroke-linecap="round"
|
||||||
<path
|
stroke-linejoin="round" />
|
||||||
:stroke-width="props.strokeWidth"
|
<path
|
||||||
d="M6 24.0083V42H42V24"
|
:stroke-width="props.strokeWidth"
|
||||||
stroke="currentColor"
|
d="M23.9917 6V32"
|
||||||
stroke-linecap="round"
|
stroke="currentColor"
|
||||||
stroke-linejoin="round" />
|
stroke-linecap="round"
|
||||||
<path
|
stroke-linejoin="round" />
|
||||||
: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>
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -11,19 +11,19 @@ const props = defineProps({
|
||||||
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
:stroke-width="props.strokeWidth"
|
:stroke-width="props.strokeWidth"
|
||||||
d="M6 24.0083V42H42V24"
|
d="M6 24V42H42V24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round" />
|
stroke-linejoin="round" />
|
||||||
<path
|
<path
|
||||||
:stroke-width="props.strokeWidth"
|
:stroke-width="props.strokeWidth"
|
||||||
d="M33 23L24 32L15 23"
|
d="M33 15L24 6L15 15"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round" />
|
stroke-linejoin="round" />
|
||||||
<path
|
<path
|
||||||
:stroke-width="props.strokeWidth"
|
:stroke-width="props.strokeWidth"
|
||||||
d="M23.9917 6V32"
|
d="M24 6V32"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round" />
|
stroke-linejoin="round" />
|
||||||
|
|
|
@ -45,7 +45,7 @@ const onUpdate = (val) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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-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-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" />
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
|
|
|
@ -41,7 +41,7 @@ defineExpose({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<n-dynamic-input v-model:value="zset" @create="onCreate" @update:value="onUpdate">
|
||||||
<template #default="{ value }">
|
<template #default="{ value }">
|
||||||
<n-input
|
<n-input
|
||||||
|
|
|
@ -25,6 +25,7 @@ import Close from '@/components/icons/Close.vue'
|
||||||
import More from '@/components/icons/More.vue'
|
import More from '@/components/icons/More.vue'
|
||||||
import Export from '@/components/icons/Export.vue'
|
import Export from '@/components/icons/Export.vue'
|
||||||
import { ConnectionType } from '@/consts/connection_type.js'
|
import { ConnectionType } from '@/consts/connection_type.js'
|
||||||
|
import Import from '@/components/icons/Import.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
server: String,
|
server: String,
|
||||||
|
@ -64,6 +65,7 @@ const dbSelectOptions = computed(() => {
|
||||||
|
|
||||||
const moreOptions = computed(() => {
|
const moreOptions = computed(() => {
|
||||||
return [
|
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: 'flush', label: i18n.t('interface.flush_db'), icon: render.renderIcon(Delete, { strokeWidth: 3.5 }) },
|
||||||
{
|
{
|
||||||
key: 'disconnect',
|
key: 'disconnect',
|
||||||
|
@ -162,6 +164,10 @@ const onExportChecked = () => {
|
||||||
browserTreeRef.value?.exportCheckedItems()
|
browserTreeRef.value?.exportCheckedItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onImportData = () => {
|
||||||
|
dialogStore.openImportKeyDialog(props.server, props.db)
|
||||||
|
}
|
||||||
|
|
||||||
const onFlush = () => {
|
const onFlush = () => {
|
||||||
dialogStore.openFlushDBDialog(props.server, props.db)
|
dialogStore.openFlushDBDialog(props.server, props.db)
|
||||||
}
|
}
|
||||||
|
@ -215,6 +221,9 @@ const onMatchInput = (matchVal, filterVal) => {
|
||||||
|
|
||||||
const onSelectOptions = (select) => {
|
const onSelectOptions = (select) => {
|
||||||
switch (select) {
|
switch (select) {
|
||||||
|
case 'import':
|
||||||
|
onImportData()
|
||||||
|
break
|
||||||
case 'flush':
|
case 'flush':
|
||||||
onFlush()
|
onFlush()
|
||||||
break
|
break
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"rename_key": "Rename Key",
|
"rename_key": "Rename Key",
|
||||||
"delete_key": "Delete Key",
|
"delete_key": "Delete Key",
|
||||||
"batch_delete_key": "Batch Delete Keys",
|
"batch_delete_key": "Batch Delete Keys",
|
||||||
|
"import_key": "Import Key",
|
||||||
"flush_db": "Flush Database",
|
"flush_db": "Flush Database",
|
||||||
"check_mode": "Check Mode",
|
"check_mode": "Check Mode",
|
||||||
"quit_check_mode": "Quit Check Mode",
|
"quit_check_mode": "Quit Check Mode",
|
||||||
|
@ -225,8 +226,7 @@
|
||||||
},
|
},
|
||||||
"cluster": {
|
"cluster": {
|
||||||
"title": "Cluster",
|
"title": "Cluster",
|
||||||
"enable": "Serve as Cluster Node",
|
"enable": "Serve as Cluster Node"
|
||||||
"readonly": "Enables read-only commands on slave nodes"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
|
@ -252,6 +252,7 @@
|
||||||
"field": {
|
"field": {
|
||||||
"new": "Add New Field",
|
"new": "Add New Field",
|
||||||
"new_item": "Add New Item",
|
"new_item": "Add New Item",
|
||||||
|
"conflict_handle": "When Field Conflict",
|
||||||
"overwrite_field": "Overwrite Existing Field",
|
"overwrite_field": "Overwrite Existing Field",
|
||||||
"ignore_field": "Ignore Existing Field",
|
"ignore_field": "Ignore Existing Field",
|
||||||
"insert_type": "Insert",
|
"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."
|
"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": {
|
"export": {
|
||||||
"name": "Export Keys",
|
"name": "Export Data",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"save_file": "Export Path",
|
"save_file": "Export Path",
|
||||||
"save_file_tip": "Select the export file save path",
|
"save_file_tip": "Select the path to save exported file",
|
||||||
"exporting": "Exporting key({index}/{count}): {key}",
|
"exporting": "Exporting keys({index}/{count})",
|
||||||
"export_completed": "Export process has been completed, {success} successed, {fail} failed"
|
"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": {
|
"ttl": {
|
||||||
"title": "Set Key TTL"
|
"title": "Set Key TTL"
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
"rename_key": "重命名键",
|
"rename_key": "重命名键",
|
||||||
"delete_key": "删除键",
|
"delete_key": "删除键",
|
||||||
"batch_delete_key": "批量删除键",
|
"batch_delete_key": "批量删除键",
|
||||||
|
"import_key": "导入数据",
|
||||||
"flush_db": "清空数据库",
|
"flush_db": "清空数据库",
|
||||||
"check_mode": "勾选模式",
|
"check_mode": "勾选模式",
|
||||||
"quit_check_mode": "退出勾选模式",
|
"quit_check_mode": "退出勾选模式",
|
||||||
|
@ -251,8 +252,9 @@
|
||||||
"field": {
|
"field": {
|
||||||
"new": "添加新字段",
|
"new": "添加新字段",
|
||||||
"new_item": "添加新元素",
|
"new_item": "添加新元素",
|
||||||
"overwrite_field": "覆盖同名字段",
|
"conflict_handle": "字段冲突处理",
|
||||||
"ignore_field": "忽略同名字段",
|
"overwrite_field": "覆盖",
|
||||||
|
"ignore_field": "忽略",
|
||||||
"insert_type": "插入类型",
|
"insert_type": "插入类型",
|
||||||
"append_item": "尾部追加",
|
"append_item": "尾部追加",
|
||||||
"prepend_item": "插入头部",
|
"prepend_item": "插入头部",
|
||||||
|
@ -271,13 +273,24 @@
|
||||||
"filter_pattern_tip": "*:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义"
|
"filter_pattern_tip": "*:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"name": "导出键",
|
"name": "导出数据",
|
||||||
"export": "确认导出",
|
"export": "确认导出",
|
||||||
"save_file": "导出路径",
|
"save_file": "导出路径",
|
||||||
"save_file_tip": "选择保存文件路径",
|
"save_file_tip": "选择导出文件保存路径",
|
||||||
"exporting": "正在导出键({index}/{count}):{key}",
|
"exporting": "正在导出键({index}/{count})",
|
||||||
"export_completed": "已完成导出操作,成功{success}个,失败{fail}个"
|
"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": {
|
"ttl": {
|
||||||
"title": "设置键存活时间"
|
"title": "设置键存活时间"
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
GetKeySummary,
|
GetKeySummary,
|
||||||
GetKeyType,
|
GetKeyType,
|
||||||
GetSlowLogs,
|
GetSlowLogs,
|
||||||
|
ImportCSV,
|
||||||
LoadAllKeys,
|
LoadAllKeys,
|
||||||
LoadNextAllKeys,
|
LoadNextAllKeys,
|
||||||
LoadNextKeys,
|
LoadNextKeys,
|
||||||
|
@ -1527,7 +1528,7 @@ const useBrowserStore = defineStore('browser', {
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async deleteKeys(server, db, keys) {
|
async deleteKeys(server, db, keys) {
|
||||||
const delMsgRef = $message.loading('', { duration: 0, closable: true })
|
const msgRef = $message.loading('', { duration: 0, closable: true })
|
||||||
let deleted = []
|
let deleted = []
|
||||||
let failCount = 0
|
let failCount = 0
|
||||||
let canceled = false
|
let canceled = false
|
||||||
|
@ -1542,13 +1543,13 @@ const useBrowserStore = defineStore('browser', {
|
||||||
maxProgress = progress
|
maxProgress = progress
|
||||||
}
|
}
|
||||||
const k = decodeRedisKey(processing)
|
const k = decodeRedisKey(processing)
|
||||||
delMsgRef.content = i18nGlobal.t('dialogue.deleting_key', {
|
msgRef.content = i18nGlobal.t('dialogue.deleting_key', {
|
||||||
key: k,
|
key: k,
|
||||||
index: maxProgress,
|
index: maxProgress,
|
||||||
count: total,
|
count: total,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
delMsgRef.onClose = () => {
|
msgRef.onClose = () => {
|
||||||
EventsEmit(cancelEvent)
|
EventsEmit(cancelEvent)
|
||||||
}
|
}
|
||||||
const { data, success, msg } = await DeleteKeys(server, db, keys, serialNo)
|
const { data, success, msg } = await DeleteKeys(server, db, keys, serialNo)
|
||||||
|
@ -1560,7 +1561,7 @@ const useBrowserStore = defineStore('browser', {
|
||||||
$message.error(msg)
|
$message.error(msg)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
delMsgRef.destroy()
|
msgRef.destroy()
|
||||||
EventsOff(eventName)
|
EventsOff(eventName)
|
||||||
// clear checked keys
|
// clear checked keys
|
||||||
const tab = useTabStore()
|
const tab = useTabStore()
|
||||||
|
@ -1608,7 +1609,7 @@ const useBrowserStore = defineStore('browser', {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async exportKeys(server, db, keys, path) {
|
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 exported = 0
|
||||||
let failCount = 0
|
let failCount = 0
|
||||||
let canceled = false
|
let canceled = false
|
||||||
|
@ -1616,13 +1617,13 @@ const useBrowserStore = defineStore('browser', {
|
||||||
try {
|
try {
|
||||||
EventsOn(eventName, ({ total, progress, processing }) => {
|
EventsOn(eventName, ({ total, progress, processing }) => {
|
||||||
// update export progress
|
// update export progress
|
||||||
delMsgRef.content = i18nGlobal.t('dialogue.export.exporting', {
|
msgRef.content = i18nGlobal.t('dialogue.export.exporting', {
|
||||||
key: decodeRedisKey(processing),
|
// key: decodeRedisKey(processing),
|
||||||
index: progress,
|
index: progress,
|
||||||
count: total,
|
count: total,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
delMsgRef.onClose = () => {
|
msgRef.onClose = () => {
|
||||||
EventsEmit('export:stop:' + path)
|
EventsEmit('export:stop:' + path)
|
||||||
}
|
}
|
||||||
const { data, success, msg } = await ExportKey(server, db, keys, path)
|
const { data, success, msg } = await ExportKey(server, db, keys, path)
|
||||||
|
@ -1634,7 +1635,7 @@ const useBrowserStore = defineStore('browser', {
|
||||||
$message.error(msg)
|
$message.error(msg)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
delMsgRef.destroy()
|
msgRef.destroy()
|
||||||
EventsOff(eventName)
|
EventsOff(eventName)
|
||||||
}
|
}
|
||||||
if (canceled) {
|
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
|
* flush database
|
||||||
* @param server
|
* @param server
|
||||||
|
|
|
@ -70,6 +70,12 @@ const useDialogStore = defineStore('dialog', {
|
||||||
},
|
},
|
||||||
exportKeyDialogVisible: false,
|
exportKeyDialogVisible: false,
|
||||||
|
|
||||||
|
importKeyParam: {
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
},
|
||||||
|
importKeyDialogVisible: false,
|
||||||
|
|
||||||
flushDBParam: {
|
flushDBParam: {
|
||||||
server: '',
|
server: '',
|
||||||
db: 0,
|
db: 0,
|
||||||
|
@ -199,6 +205,20 @@ const useDialogStore = defineStore('dialog', {
|
||||||
this.exportKeyDialogVisible = false
|
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) {
|
openFlushDBDialog(server, db) {
|
||||||
this.flushDBParam.server = server
|
this.flushDBParam.server = server
|
||||||
this.flushDBParam.db = db
|
this.flushDBParam.db = db
|
||||||
|
|
Loading…
Reference in New Issue