perf: add silent deletion option to batch delete dialog

This commit is contained in:
Lykin 2024-01-23 11:26:15 +08:00
parent ff2043c0e2
commit a679858478
5 changed files with 118 additions and 79 deletions

View File

@ -2073,7 +2073,7 @@ func (b *browserService) DeleteOneKey(server string, db int, k any) (resp types.
} }
// DeleteKeys delete keys sync with notification // DeleteKeys delete keys sync with notification
func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo string) (resp types.JSResp) { func (b *browserService) DeleteKeys(server string, db int, ks []any, notice bool, serialNo string) (resp types.JSResp) {
// connect a new connection to export keys // connect a new connection to export keys
conf := Connection().getConnection(server) conf := Connection().getConnection(server)
if conf == nil { if conf == nil {
@ -2093,7 +2093,7 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
defer cancelFunc() defer cancelFunc()
cancelEvent := "delete:stop:" + serialNo cancelEvent := "delete:stop:" + serialNo
runtime.EventsOnce(ctx, cancelEvent, func(data ...any) { cancelStopEvent := runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
cancelFunc() cancelFunc()
}) })
processEvent := "deleting:" + serialNo processEvent := "deleting:" + serialNo
@ -2104,29 +2104,41 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
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().Add(-10 * time.Second) startTime := time.Now().Add(-10 * time.Second)
supportUnlink := true
for i, k := range ks { for i, k := range ks {
// emit progress per second // emit progress per second
if i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100 { if notice && (i >= total-1 || time.Now().Sub(startTime).Milliseconds() > 100) {
startTime = time.Now() startTime = time.Now()
param := map[string]any{ param := map[string]any{
"total": total, "total": total,
"progress": i + 1, "progress": i + 1,
"processing": k, "processing": k,
} }
runtime.EventsEmit(b.ctx, processEvent, param) runtime.EventsEmit(ctx, processEvent, param)
// do some sleep to prevent blocking the Redis server // do some sleep to prevent blocking the Redis server
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
delErr := cli.Del(ctx, key).Err() var delErr error
if err != nil { if supportUnlink {
failed.Add(1) if delErr = cli.Unlink(ctx, key).Err(); delErr != nil {
} else { supportUnlink = false
// save deleted key delErr = nil
mutex.Lock() }
deletedKeys = append(deletedKeys, k) }
mutex.Unlock() if !supportUnlink {
delErr = cli.Del(ctx, key).Err()
}
if notice {
if delErr != nil {
failed.Add(1)
} else {
// save deleted key
mutex.Lock()
deletedKeys = append(deletedKeys, k)
mutex.Unlock()
}
} }
if errors.Is(delErr, context.Canceled) || canceled { if errors.Is(delErr, context.Canceled) || canceled {
canceled = true canceled = true
@ -2145,7 +2157,7 @@ func (b *browserService) DeleteKeys(server string, db int, ks []any, serialNo st
err = del(ctx, client) err = del(ctx, client)
} }
runtime.EventsOff(ctx, cancelEvent) cancelStopEvent()
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = struct {
Canceled bool `json:"canceled"` Canceled bool `json:"canceled"`
@ -2189,8 +2201,7 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string,
writer := csv.NewWriter(file) writer := csv.NewWriter(file)
defer writer.Flush() defer writer.Flush()
cancelEvent := "export:stop:" + path cancelStopEvent := runtime.EventsOnce(ctx, "export:stop:"+path, func(data ...any) {
runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
cancelFunc() cancelFunc()
}) })
processEvent := "exporting:" + path processEvent := "exporting:" + path
@ -2206,7 +2217,7 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string,
"progress": i + 1, "progress": i + 1,
"processing": k, "processing": k,
} }
runtime.EventsEmit(b.ctx, processEvent, param) runtime.EventsEmit(ctx, processEvent, param)
} }
key := strutil.DecodeRedisKey(k) key := strutil.DecodeRedisKey(k)
@ -2230,7 +2241,7 @@ func (b *browserService) ExportKey(server string, db int, ks []any, path string,
} }
} }
runtime.EventsOff(ctx, cancelEvent) cancelStopEvent()
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = struct {
Canceled bool `json:"canceled"` Canceled bool `json:"canceled"`
@ -2274,7 +2285,7 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
reader := csv.NewReader(file) reader := csv.NewReader(file)
cancelEvent := "import:stop:" + path cancelEvent := "import:stop:" + path
runtime.EventsOnce(ctx, cancelEvent, func(data ...any) { cancelStopEvent := runtime.EventsOnce(ctx, cancelEvent, func(data ...any) {
cancelFunc() cancelFunc()
}) })
processEvent := "importing:" + path processEvent := "importing:" + path
@ -2349,7 +2360,7 @@ func (b *browserService) ImportCSV(server string, db int, path string, conflict
} }
} }
runtime.EventsOff(ctx, cancelEvent) cancelStopEvent()
resp.Success = true resp.Success = true
resp.Data = struct { resp.Data = struct {
Canceled bool `json:"canceled"` Canceled bool `json:"canceled"`

View File

@ -74,7 +74,7 @@ const onConfirmDelete = async () => {
deleting.value = true deleting.value = true
const { server, db, key, affectedKeys } = deleteForm const { server, db, key, affectedKeys } = deleteForm
await nextTick() await nextTick()
browserStore.deleteKeys(server, db, affectedKeys).catch((e) => {}) browserStore.deleteKeys(server, db, affectedKeys, !deleteForm.async).catch((e) => {})
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
return return
@ -115,11 +115,9 @@ const onClose = () => {
required> required>
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" /> <n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
</n-form-item> </n-form-item>
<!-- <n-form-item :label="$t('dialogue.key.async_delete')" required>--> <n-checkbox v-model:checked="deleteForm.async">
<!-- <n-checkbox v-model:checked="deleteForm.async">--> {{ $t('dialogue.key.silent') }}
<!-- {{ $t('dialogue.key.async_delete_title') }}--> </n-checkbox>
<!-- </n-checkbox>-->
<!-- </n-form-item>-->
<n-card <n-card
v-if="deleteForm.showAffected" v-if="deleteForm.showAffected"
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`" :title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"

View File

@ -263,13 +263,16 @@
"confirm_delete_key": "Confirm Delete {num} Key(s)", "confirm_delete_key": "Confirm Delete {num} Key(s)",
"async_delete": "Asynchronously Execute", "async_delete": "Asynchronously Execute",
"async_delete_title": "Do not waiting for the operation's result", "async_delete_title": "Do not waiting for the operation's result",
"silent": "Do not display deletion status in real time",
"confirm_flush": "I know what I'm doing!", "confirm_flush": "I know what I'm doing!",
"confirm_flush_db": "Confirm Flush Database" "confirm_flush_db": "Confirm Flush Database"
}, },
"delete": { "delete": {
"success": "\"{key}\" has been deleted", "success": "\"{key}\" has been deleted",
"deleting": "Deleting",
"doing": "Deleting key({index}/{count})", "doing": "Deleting key({index}/{count})",
"completed": "Deletion process has been completed, {success} successed, {fail} failed" "completed": "Deletion process has been completed",
"completed_status": "{success} successed, {fail} failed"
}, },
"field": { "field": {
"new": "Add New Field", "new": "Add New Field",

View File

@ -263,13 +263,16 @@
"confirm_delete_key": "确认删除{num}个键", "confirm_delete_key": "确认删除{num}个键",
"async_delete": "异步执行", "async_delete": "异步执行",
"async_delete_title": "不等待操作结果", "async_delete_title": "不等待操作结果",
"silent": "不实时显示删除状态",
"confirm_flush": "我知道我正在执行的操作!", "confirm_flush": "我知道我正在执行的操作!",
"confirm_flush_db": "确认清空数据库" "confirm_flush_db": "确认清空数据库"
}, },
"delete": { "delete": {
"success": "{key} 已被删除", "success": "{key} 已被删除",
"deleting": "正在删除",
"doing": "正在删除键({index}/{count})", "doing": "正在删除键({index}/{count})",
"completed": "已完成删除操作,成功{success}个,失败{fail}个" "completed": "已完成删除操作",
"completed_status": "成功{success}个,失败{fail}个"
}, },
"field": { "field": {
"new": "添加新字段", "new": "添加新字段",

View File

@ -44,7 +44,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' import { EventsEmit, EventsOn } from 'wailsjs/runtime/runtime.js'
import { RedisNodeItem } from '@/objects/redisNodeItem.js' import { RedisNodeItem } from '@/objects/redisNodeItem.js'
import { RedisServerState } from '@/objects/redisServerState.js' import { RedisServerState } from '@/objects/redisServerState.js'
import { RedisDatabaseItem } from '@/objects/redisDatabaseItem.js' import { RedisDatabaseItem } from '@/objects/redisDatabaseItem.js'
@ -1614,34 +1614,33 @@ const useBrowserStore = defineStore('browser', {
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string[]|number[][]} keys * @param {string[]|number[][]} keys
* @param {boolean} notice
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async deleteKeys(server, db, keys) { async deleteKeys(server, db, keys, notice) {
const msgRef = $message.loading('', { duration: 0, closable: true }) const msgRef = $message.loading(i18nGlobal.t('dialogue.delete.deleting'), { duration: 0, closable: true })
let deleted = [] let deleted = []
let failCount = 0 let failCount = 0
let canceled = false let canceled = false
const serialNo = Date.now().valueOf().toString() const serialNo = Date.now().valueOf().toString()
const eventName = 'deleting:' + serialNo let maxProgress = 0
const cancelEvent = 'delete:stop:' + serialNo const cancelEventFn = EventsOn('deleting:' + serialNo, ({ total, progress, processing }) => {
try { // update delete progress
let maxProgress = 0 if (progress > maxProgress) {
EventsOn(eventName, ({ total, progress, processing }) => { maxProgress = progress
// update delete progress
if (progress > maxProgress) {
maxProgress = progress
}
const k = decodeRedisKey(processing)
msgRef.content = i18nGlobal.t('dialogue.delete.doing', {
key: k,
index: maxProgress,
count: total,
})
})
msgRef.onClose = () => {
EventsEmit(cancelEvent)
} }
const { data, success, msg } = await DeleteKeys(server, db, keys, serialNo) const k = decodeRedisKey(processing)
msgRef.content = i18nGlobal.t('dialogue.delete.doing', {
key: k,
index: maxProgress,
count: total,
})
})
msgRef.onClose = () => {
EventsEmit('delete:stop:' + serialNo)
}
try {
const { data, success, msg } = await DeleteKeys(server, db, keys, notice, serialNo)
if (success) { if (success) {
canceled = get(data, 'canceled', false) canceled = get(data, 'canceled', false)
deleted = get(data, 'deleted', []) deleted = get(data, 'deleted', [])
@ -1650,8 +1649,8 @@ const useBrowserStore = defineStore('browser', {
$message.error(msg) $message.error(msg)
} }
} finally { } finally {
cancelEventFn()
msgRef.destroy() msgRef.destroy()
EventsOff(eventName)
// clear checked keys // clear checked keys
const tab = useTabStore() const tab = useTabStore()
tab.setCheckedKeys(server) tab.setCheckedKeys(server)
@ -1662,13 +1661,40 @@ const useBrowserStore = defineStore('browser', {
$message.info(i18nGlobal.t('dialogue.handle_cancel')) $message.info(i18nGlobal.t('dialogue.handle_cancel'))
} else if (failCount <= 0) { } else if (failCount <= 0) {
// no fail // no fail
$message.success(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount })) let msg = i18nGlobal.t('dialogue.delete.completed')
if (notice) {
msg +=
'\n' +
i18nGlobal.t('dialogue.delete.completed_status', {
success: deletedCount,
fail: failCount,
})
}
$message.success(msg)
} else if (failCount >= deletedCount) { } else if (failCount >= deletedCount) {
// all fail // all fail
$message.error(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount })) let msg = i18nGlobal.t('dialogue.delete.completed')
if (notice) {
msg +=
'\n' +
i18nGlobal.t('dialogue.delete.completed_status', {
success: deletedCount,
fail: failCount,
})
}
$message.error(msg)
} else { } else {
// some fail // some fail
$message.warn(i18nGlobal.t('dialogue.delete.completed', { success: deletedCount, fail: failCount })) let msg = i18nGlobal.t('dialogue.delete.completed')
if (notice) {
msg +=
'\n' +
i18nGlobal.t('dialogue.delete.completed_status', {
success: deletedCount,
fail: failCount,
})
}
$message.warn(msg)
} }
// update ui // update ui
timeout(100).then(async () => { timeout(100).then(async () => {
@ -1703,19 +1729,18 @@ const useBrowserStore = defineStore('browser', {
let exported = 0 let exported = 0
let failCount = 0 let failCount = 0
let canceled = false let canceled = false
const eventName = 'exporting:' + path const cancelEventFn = EventsOn('exporting:' + path, ({ total, progress, processing }) => {
try { // update export progress
EventsOn(eventName, ({ total, progress, processing }) => { msgRef.content = i18nGlobal.t('dialogue.export.exporting', {
// update export progress // key: decodeRedisKey(processing),
msgRef.content = i18nGlobal.t('dialogue.export.exporting', { index: progress,
// key: decodeRedisKey(processing), count: total,
index: progress,
count: total,
})
}) })
msgRef.onClose = () => { })
EventsEmit('export:stop:' + path) msgRef.onClose = () => {
} EventsEmit('export:stop:' + path)
}
try {
const { data, success, msg } = await ExportKey(server, db, keys, path, expire) const { data, success, msg } = await ExportKey(server, db, keys, path, expire)
if (success) { if (success) {
canceled = get(data, 'canceled', false) canceled = get(data, 'canceled', false)
@ -1726,7 +1751,7 @@ const useBrowserStore = defineStore('browser', {
} }
} finally { } finally {
msgRef.destroy() msgRef.destroy()
EventsOff(eventName) cancelEventFn()
} }
if (canceled) { if (canceled) {
$message.info(i18nGlobal.t('dialogue.handle_cancel')) $message.info(i18nGlobal.t('dialogue.handle_cancel'))
@ -1759,19 +1784,18 @@ const useBrowserStore = defineStore('browser', {
let imported = 0 let imported = 0
let ignored = 0 let ignored = 0
let canceled = false let canceled = false
const eventName = 'importing:' + path const cancelEventFn = EventsOn('importing:' + path, ({ imported = 0, ignored = 0 }) => {
try { // update export progress
EventsOn(eventName, ({ imported = 0, ignored = 0 }) => { msgRef.content = i18nGlobal.t('dialogue.import.importing', {
// update export progress // key: decodeRedisKey(processing),
msgRef.content = i18nGlobal.t('dialogue.import.importing', { imported,
// key: decodeRedisKey(processing), conflict: ignored,
imported,
conflict: ignored,
})
}) })
msgRef.onClose = () => { })
EventsEmit('import:stop:' + path) msgRef.onClose = () => {
} EventsEmit('import:stop:' + path)
}
try {
const { data, success, msg } = await ImportCSV(server, db, path, conflict, ttl) const { data, success, msg } = await ImportCSV(server, db, path, conflict, ttl)
if (success) { if (success) {
canceled = get(data, 'canceled', false) canceled = get(data, 'canceled', false)
@ -1781,8 +1805,8 @@ const useBrowserStore = defineStore('browser', {
$message.error(msg) $message.error(msg)
} }
} finally { } finally {
cancelEventFn()
msgRef.destroy() msgRef.destroy()
EventsOff(eventName)
} }
if (canceled) { if (canceled) {
$message.info(i18nGlobal.t('dialogue.handle_cancel')) $message.info(i18nGlobal.t('dialogue.handle_cancel'))