mirror of
				https://github.com/tiny-craft/tiny-rdm.git
				synced 2025-11-01 02:52:33 +08:00 
			
		
		
		
	feat: support import keys from csv file
This commit is contained in:
		| @@ -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> | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								frontend/src/components/dialogs/ImportKeyDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								frontend/src/components/dialogs/ImportKeyDialog.vue
									
									
									
									
									
										Normal 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> | ||||||
| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Lykin
					Lykin