feat: add export capability for selected keys
This commit is contained in:
parent
b06217adc0
commit
bce4e2323e
|
@ -2,11 +2,15 @@ package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -1936,6 +1940,77 @@ func (b *browserService) DeleteOneKey(server string, db int, k any) (resp types.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportKey export keys
|
||||||
|
func (b *browserService) ExportKey(server string, db int, ks []any, path string) (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 = Connection().createRedisClient(connConfig); err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: add cancel handle
|
||||||
|
ctx, cancelFunc := context.WithCancel(b.ctx)
|
||||||
|
defer cancelFunc()
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
writer := csv.NewWriter(file)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
runtime.EventsOnce(ctx, "export:stop:"+path, func(data ...any) {
|
||||||
|
cancelFunc()
|
||||||
|
})
|
||||||
|
processEvent := "exporting:" + path
|
||||||
|
total := len(ks)
|
||||||
|
var exported, failed int64
|
||||||
|
var canceled bool
|
||||||
|
for i, k := range ks {
|
||||||
|
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()
|
||||||
|
if errors.Is(dumpErr, context.Canceled) {
|
||||||
|
canceled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err = writer.Write([]string{hex.EncodeToString([]byte(key)), hex.EncodeToString(content)}); err != nil {
|
||||||
|
failed += 1
|
||||||
|
} else {
|
||||||
|
exported += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = struct {
|
||||||
|
Canceled bool `json:"canceled"`
|
||||||
|
Exported int64 `json:"exported"`
|
||||||
|
Failed int64 `json:"failed"`
|
||||||
|
}{
|
||||||
|
Canceled: canceled,
|
||||||
|
Exported: exported,
|
||||||
|
Failed: failed,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
|
@ -127,6 +127,9 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
||||||
WriteTimeout: time.Duration(config.ExecTimeout) * time.Second,
|
WriteTimeout: time.Duration(config.ExecTimeout) * time.Second,
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
if config.LastDB > 0 {
|
||||||
|
option.DB = config.LastDB
|
||||||
|
}
|
||||||
if sshClient != nil {
|
if sshClient != nil {
|
||||||
option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return sshClient.Dial(network, addr)
|
return sshClient.Dial(network, addr)
|
||||||
|
|
|
@ -3,11 +3,11 @@ package services
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"tinyrdm/backend/consts"
|
"tinyrdm/backend/consts"
|
||||||
"tinyrdm/backend/types"
|
"tinyrdm/backend/types"
|
||||||
|
sliceutil "tinyrdm/backend/utils/slice"
|
||||||
)
|
)
|
||||||
|
|
||||||
type systemService struct {
|
type systemService struct {
|
||||||
|
@ -50,7 +50,30 @@ func (s *systemService) SelectFile(title string) (resp types.JSResp) {
|
||||||
ShowHiddenFiles: true,
|
ShowHiddenFiles: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"path": filepath,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFile open file dialog to save a file
|
||||||
|
func (s *systemService) SaveFile(title string, defaultName string, extensions []string) (resp types.JSResp) {
|
||||||
|
filters := sliceutil.Map(extensions, func(i int) runtime.FileFilter {
|
||||||
|
return runtime.FileFilter{
|
||||||
|
Pattern: "*." + extensions[i],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
filepath, err := runtime.SaveFileDialog(s.ctx, runtime.SaveDialogOptions{
|
||||||
|
Title: title,
|
||||||
|
ShowHiddenFiles: true,
|
||||||
|
DefaultFilename: defaultName,
|
||||||
|
Filters: filters,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
resp.Msg = err.Error()
|
resp.Msg = err.Error()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { WindowSetDarkTheme, WindowSetLightTheme } from 'wailsjs/runtime/runtime
|
||||||
import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
|
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'
|
||||||
|
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
const connectionStore = useConnectionStore()
|
const connectionStore = useConnectionStore()
|
||||||
|
@ -67,6 +68,7 @@ watch(
|
||||||
<add-fields-dialog />
|
<add-fields-dialog />
|
||||||
<rename-key-dialog />
|
<rename-key-dialog />
|
||||||
<delete-key-dialog />
|
<delete-key-dialog />
|
||||||
|
<export-key-dialog />
|
||||||
<flush-db-dialog />
|
<flush-db-dialog />
|
||||||
<set-ttl-dialog />
|
<set-ttl-dialog />
|
||||||
<preferences-dialog />
|
<preferences-dialog />
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script setup>
|
||||||
|
import { SaveFile } from 'wailsjs/go/services/systemService.js'
|
||||||
|
import { get } from 'lodash'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: String,
|
||||||
|
placeholder: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
defaultPath: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
|
const onClear = () => {
|
||||||
|
emit('update:value', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveFile = async () => {
|
||||||
|
const { success, data } = await SaveFile(null, props.defaultPath, ['csv'])
|
||||||
|
if (success) {
|
||||||
|
const path = get(data, 'path', '')
|
||||||
|
emit('update:value', path)
|
||||||
|
} else {
|
||||||
|
emit('update:value', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input
|
||||||
|
v-model:value="props.value"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
clearable
|
||||||
|
@clear="onClear" />
|
||||||
|
<n-button :disabled="props.disabled" :focusable="false" @click="handleSaveFile">...</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -99,10 +99,10 @@ const onClose = () => {
|
||||||
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-grid :x-gap="10">
|
<n-grid :x-gap="10">
|
||||||
<n-form-item-gi :label="$t('dialogue.key.server')" :span="12">
|
<n-form-item-gi :label="$t('dialogue.key.server')" :span="12">
|
||||||
<n-input :value="deleteForm.server" readonly />
|
<n-input :autofocus="false" :value="deleteForm.server" readonly />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
|
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
|
||||||
<n-input :value="deleteForm.db.toString()" readonly />
|
<n-input :autofocus="false" :value="deleteForm.db.toString()" readonly />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<n-form-item
|
<n-form-item
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
<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 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()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="dialogStore.exportKeyDialogVisible"
|
||||||
|
:closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="$t('dialogue.export.name')"
|
||||||
|
preset="dialog"
|
||||||
|
transform-origin="center">
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<n-form :model="exportKeyForm" :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="exportKeyForm.server" readonly />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('dialogue.key.db_index')" :span="12">
|
||||||
|
<n-input :autofocus="false" :value="exportKeyForm.db.toString()" readonly />
|
||||||
|
</n-form-item-gi>
|
||||||
|
</n-grid>
|
||||||
|
<n-form-item :label="$t('dialogue.export.save_file')" required>
|
||||||
|
<file-save-input
|
||||||
|
v-model:value="exportKeyForm.file"
|
||||||
|
:default-path="`export_${dayjs().format('YYYYMMDDHHmmss')}.csv`"
|
||||||
|
:placeholder="$t('dialogue.export.save_file_tip')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-card
|
||||||
|
:title="$t('dialogue.key.affected_key') + `(${size(exportKeyForm.keys)})`"
|
||||||
|
embedded
|
||||||
|
size="small">
|
||||||
|
<n-log :line-height="1.5" :lines="keyLines" :rows="10" style="user-select: text; cursor: text" />
|
||||||
|
</n-card>
|
||||||
|
</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="!exportEnable"
|
||||||
|
:focusable="false"
|
||||||
|
:loading="loading"
|
||||||
|
type="error"
|
||||||
|
@click="onConfirmDelete">
|
||||||
|
{{ $t('dialogue.export.export') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -23,6 +23,7 @@ import useConnectionStore from 'stores/connections.js'
|
||||||
import ListCheckbox from '@/components/icons/ListCheckbox.vue'
|
import ListCheckbox from '@/components/icons/ListCheckbox.vue'
|
||||||
import Close from '@/components/icons/Close.vue'
|
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'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
server: String,
|
server: String,
|
||||||
|
@ -146,6 +147,10 @@ const onDeleteChecked = () => {
|
||||||
browserTreeRef.value?.deleteCheckedItems()
|
browserTreeRef.value?.deleteCheckedItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onExportChecked = () => {
|
||||||
|
browserTreeRef.value?.exportCheckedItems()
|
||||||
|
}
|
||||||
|
|
||||||
const onFlush = () => {
|
const onFlush = () => {
|
||||||
dialogStore.openFlushDBDialog(props.server, props.db)
|
dialogStore.openFlushDBDialog(props.server, props.db)
|
||||||
}
|
}
|
||||||
|
@ -329,6 +334,14 @@ onMounted(() => onReload())
|
||||||
|
|
||||||
<!-- check mode function bar -->
|
<!-- check mode function bar -->
|
||||||
<div v-else class="flex-box-h nav-pane-func">
|
<div v-else class="flex-box-h nav-pane-func">
|
||||||
|
<icon-button
|
||||||
|
:button-class="['nav-pane-func-btn']"
|
||||||
|
:disabled="checkedCount <= 0"
|
||||||
|
:icon="Export"
|
||||||
|
:stroke-width="3.5"
|
||||||
|
size="20"
|
||||||
|
t-tooltip="interface.export_checked"
|
||||||
|
@click="onExportChecked" />
|
||||||
<icon-button
|
<icon-button
|
||||||
:button-class="['nav-pane-func-btn']"
|
:button-class="['nav-pane-func-btn']"
|
||||||
:disabled="checkedCount <= 0"
|
:disabled="checkedCount <= 0"
|
||||||
|
|
|
@ -581,6 +581,13 @@ defineExpose({
|
||||||
dialogStore.openDeleteKeyDialog(props.server, props.db, redisKeys)
|
dialogStore.openDeleteKeyDialog(props.server, props.db, redisKeys)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
exportCheckedItems: () => {
|
||||||
|
const checkedKeys = tabStore.currentCheckedKeys
|
||||||
|
const redisKeys = map(checkedKeys, 'redisKey')
|
||||||
|
if (!isEmpty(redisKeys)) {
|
||||||
|
dialogStore.openExportKeyDialog(props.server, props.db, redisKeys)
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
"check_mode": "Check Mode",
|
"check_mode": "Check Mode",
|
||||||
"quit_check_mode": "Quit Check Mode",
|
"quit_check_mode": "Quit Check Mode",
|
||||||
"delete_checked": "Delete Checked Items",
|
"delete_checked": "Delete Checked Items",
|
||||||
|
"export_checked": "Export Checked Items",
|
||||||
"copy_value": "Copy Value",
|
"copy_value": "Copy Value",
|
||||||
"edit_value": "Edit Value",
|
"edit_value": "Edit Value",
|
||||||
"save_update": "Save Update",
|
"save_update": "Save Update",
|
||||||
|
@ -146,6 +147,7 @@
|
||||||
"delete_completed": "Deletion process has been completed, {success} successed, {fail} failed",
|
"delete_completed": "Deletion process has been completed, {success} successed, {fail} failed",
|
||||||
"rename_binary_key_fail": "Rename binary key name is unsupported",
|
"rename_binary_key_fail": "Rename binary key name is unsupported",
|
||||||
"handle_succ": "Success!",
|
"handle_succ": "Success!",
|
||||||
|
"handle_cancel": "The operation has been canceled.",
|
||||||
"reload_succ": "Reloaded!",
|
"reload_succ": "Reloaded!",
|
||||||
"field_required": "This item should not be blank",
|
"field_required": "This item should not be blank",
|
||||||
"spec_field_required": "\"{key}\" should not be blank",
|
"spec_field_required": "\"{key}\" should not be blank",
|
||||||
|
@ -269,6 +271,14 @@
|
||||||
"filter_pattern": "Pattern",
|
"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."
|
"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": {
|
"ttl": {
|
||||||
"title": "Set Key TTL"
|
"title": "Set Key TTL"
|
||||||
},
|
},
|
||||||
|
|
|
@ -78,7 +78,8 @@
|
||||||
"flush_db": "清空数据库",
|
"flush_db": "清空数据库",
|
||||||
"check_mode": "勾选模式",
|
"check_mode": "勾选模式",
|
||||||
"quit_check_mode": "退出勾选模式",
|
"quit_check_mode": "退出勾选模式",
|
||||||
"delete_checked": "删除勾选项",
|
"delete_checked": "删除所选项",
|
||||||
|
"export_checked": "导出所选项",
|
||||||
"copy_value": "复制值",
|
"copy_value": "复制值",
|
||||||
"edit_value": "修改值",
|
"edit_value": "修改值",
|
||||||
"save_update": "保存修改",
|
"save_update": "保存修改",
|
||||||
|
@ -146,6 +147,7 @@
|
||||||
"delete_completed": "已完成删除操作,成功{success}个,失败{fail}个",
|
"delete_completed": "已完成删除操作,成功{success}个,失败{fail}个",
|
||||||
"rename_binary_key_fail": "不支持重命名二进制键名",
|
"rename_binary_key_fail": "不支持重命名二进制键名",
|
||||||
"handle_succ": "操作成功",
|
"handle_succ": "操作成功",
|
||||||
|
"handle_cancel": "操作已取消",
|
||||||
"reload_succ": "已重新载入",
|
"reload_succ": "已重新载入",
|
||||||
"field_required": "此项不能为空",
|
"field_required": "此项不能为空",
|
||||||
"spec_field_required": "{key} 不能为空",
|
"spec_field_required": "{key} 不能为空",
|
||||||
|
@ -268,6 +270,14 @@
|
||||||
"filter_pattern": "过滤表达式",
|
"filter_pattern": "过滤表达式",
|
||||||
"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": {
|
||||||
|
"name": "导出键",
|
||||||
|
"export": "确认导出",
|
||||||
|
"save_file": "导出路径",
|
||||||
|
"save_file_tip": "选择保存文件路径",
|
||||||
|
"exporting": "正在导出键:{key} ({index}/{count})",
|
||||||
|
"export_completed": "已完成导出操作,成功{success}个,失败{fail}个"
|
||||||
|
},
|
||||||
"ttl": {
|
"ttl": {
|
||||||
"title": "设置键存活时间"
|
"title": "设置键存活时间"
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
ConvertValue,
|
ConvertValue,
|
||||||
DeleteKey,
|
DeleteKey,
|
||||||
DeleteOneKey,
|
DeleteOneKey,
|
||||||
|
ExportKey,
|
||||||
FlushDB,
|
FlushDB,
|
||||||
GetCmdHistory,
|
GetCmdHistory,
|
||||||
GetKeyDetail,
|
GetKeyDetail,
|
||||||
|
@ -56,6 +57,7 @@ import useConnectionStore from 'stores/connections.js'
|
||||||
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
|
||||||
import { isRedisGlob } from '@/utils/glob_pattern.js'
|
import { isRedisGlob } from '@/utils/glob_pattern.js'
|
||||||
import { i18nGlobal } from '@/utils/i18n.js'
|
import { i18nGlobal } from '@/utils/i18n.js'
|
||||||
|
import { EventsEmit, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||||
|
|
||||||
const useBrowserStore = defineStore('browser', {
|
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<void>}
|
||||||
|
*/
|
||||||
|
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
|
* flush database
|
||||||
* @param connName
|
* @param connName
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from 'pinia'
|
||||||
import useConnectionStore from "./connections.js";
|
import useConnectionStore from './connections.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* connection dialog type
|
* connection dialog type
|
||||||
|
@ -63,6 +63,13 @@ const useDialogStore = defineStore('dialog', {
|
||||||
},
|
},
|
||||||
deleteKeyDialogVisible: false,
|
deleteKeyDialogVisible: false,
|
||||||
|
|
||||||
|
exportKeyParam: {
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
keys: [],
|
||||||
|
},
|
||||||
|
exportKeyDialogVisible: false,
|
||||||
|
|
||||||
flushDBParam: {
|
flushDBParam: {
|
||||||
server: '',
|
server: '',
|
||||||
db: 0,
|
db: 0,
|
||||||
|
@ -176,6 +183,22 @@ const useDialogStore = defineStore('dialog', {
|
||||||
this.deleteKeyDialogVisible = false
|
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) {
|
openFlushDBDialog(server, db) {
|
||||||
this.flushDBParam.server = server
|
this.flushDBParam.server = server
|
||||||
this.flushDBParam.db = db
|
this.flushDBParam.db = db
|
||||||
|
|
Loading…
Reference in New Issue