refactor: optimized refresh logic after update fields for hash type

This commit is contained in:
Lykin 2023-11-17 16:26:03 +08:00
parent d88fd35e9d
commit 80e659e03a
9 changed files with 308 additions and 220 deletions

View File

@ -1081,7 +1081,7 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
return return
} }
// SetHashValue set hash field // SetHashValue update hash field
func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSResp) { func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB) item, err := b.getRedisClient(param.Server, param.DB)
if err != nil { if err != nil {
@ -1092,36 +1092,64 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key) key := strutil.DecodeRedisKey(param.Key)
str := strutil.DecodeRedisKey(param.Value) str := strutil.DecodeRedisKey(param.Value)
var saveStr string var saveStr, displayStr string
if saveStr, err = strutil.SaveAs(str, param.Format, param.Decode); err != nil { if saveStr, err = strutil.SaveAs(str, param.Format, param.Decode); err != nil {
resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
return return
} }
var removedField []string displayStr, _, _ = strutil.ConvertTo(saveStr, param.RetDecode, param.RetFormat)
updatedField := map[string]any{} var updated, added, removed []types.HashEntryItem
replacedField := map[string]any{} var replaced []types.HashReplaceItem
if len(param.Field) <= 0 { var affect int64
// old filed is empty, add new field if len(param.NewField) <= 0 {
_, err = client.HSet(ctx, key, param.NewField, saveStr).Result()
updatedField[param.NewField] = saveStr
} else if len(param.NewField) <= 0 {
// new field is empty, delete old field // new field is empty, delete old field
_, err = client.HDel(ctx, key, param.Field).Result() _, err = client.HDel(ctx, key, param.Field).Result()
removedField = append(removedField, param.Field) removed = append(removed, types.HashEntryItem{
} else if param.Field == param.NewField { Key: param.Field,
// update field value })
_, err = client.HSet(ctx, key, param.Field, saveStr).Result() } else if len(param.Field) <= 0 || param.Field == param.NewField {
updatedField[param.NewField] = saveStr affect, err = client.HSet(ctx, key, param.NewField, saveStr).Result()
if affect <= 0 {
// update field value
updated = append(updated, types.HashEntryItem{
Key: param.NewField,
Value: saveStr,
DisplayValue: displayStr,
})
} else {
// add new field
added = append(added, types.HashEntryItem{
Key: param.NewField,
Value: saveStr,
DisplayValue: displayStr,
})
}
} else { } else {
// remove old field and add new field // remove old field and add new field
if _, err = client.HDel(ctx, key, param.Field).Result(); err != nil { if _, err = client.HDel(ctx, key, param.Field).Result(); err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
_, err = client.HSet(ctx, key, param.NewField, saveStr).Result() affect, err = client.HSet(ctx, key, param.NewField, saveStr).Result()
removedField = append(removedField, param.Field) if affect <= 0 {
updatedField[param.NewField] = saveStr // no new filed added, just replace exists item
replacedField[param.Field] = param.NewField removed = append(removed, types.HashEntryItem{
Key: param.Field,
})
updated = append(updated, types.HashEntryItem{
Key: param.NewField,
Value: saveStr,
DisplayValue: displayStr,
})
} else {
// add new field
replaced = append(replaced, types.HashReplaceItem{
Key: param.Field,
NewKey: param.NewField,
Value: saveStr,
DisplayValue: displayStr,
})
}
} }
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -1129,10 +1157,16 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe
} }
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = struct {
"removed": removedField, Added []types.HashEntryItem `json:"added,omitempty"`
"updated": updatedField, Removed []types.HashEntryItem `json:"removed,omitempty"`
"replaced": replacedField, Updated []types.HashEntryItem `json:"updated,omitempty"`
Replaced []types.HashReplaceItem `json:"replaced,omitempty"`
}{
Added: added,
Removed: removed,
Updated: updated,
Replaced: replaced,
} }
return return
} }
@ -1314,8 +1348,7 @@ func (b *browserService) SetSetItem(server string, db int, k any, remove bool, m
for _, member := range members { for _, member := range members {
if affected, _ = client.SRem(ctx, key, member).Result(); affected > 0 { if affected, _ = client.SRem(ctx, key, member).Result(); affected > 0 {
removed = append(removed, types.SetEntryItem{ removed = append(removed, types.SetEntryItem{
Value: member, Value: member,
DisplayValue: "", // TODO: convert to display value
}) })
} }
} }

View File

@ -60,14 +60,16 @@ type SetListParam struct {
} }
type SetHashParam struct { type SetHashParam struct {
Server string `json:"server"` Server string `json:"server"`
DB int `json:"db"` DB int `json:"db"`
Key any `json:"key"` Key any `json:"key"`
Field string `json:"field,omitempty"` Field string `json:"field,omitempty"`
NewField string `json:"newField,omitempty"` NewField string `json:"newField,omitempty"`
Value any `json:"value"` Value any `json:"value"`
Format string `json:"format,omitempty"` Format string `json:"format,omitempty"`
Decode string `json:"decode,omitempty"` Decode string `json:"decode,omitempty"`
RetFormat string `json:"retFormat,omitempty"`
RetDecode string `json:"retDecode,omitempty"`
} }
type SetSetParam struct { type SetSetParam struct {

View File

@ -5,12 +5,25 @@ type ListEntryItem struct {
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
} }
type ListReplaceItem struct {
Index int64 `json:"index"`
Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"`
}
type HashEntryItem struct { type HashEntryItem struct {
Key string `json:"k"` Key string `json:"k"`
Value any `json:"v"` Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
} }
type HashReplaceItem struct {
Key any `json:"k"`
NewKey any `json:"nk"`
Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"`
}
type SetEntryItem struct { type SetEntryItem struct {
Value any `json:"v"` Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
@ -22,6 +35,13 @@ type ZSetEntryItem struct {
DisplayValue string `json:"dv,omitempty"` DisplayValue string `json:"dv,omitempty"`
} }
type ZSetReplaceItem struct {
Score float64 `json:"s"`
Value string `json:"v"`
NewValue string `json:"nv"`
DisplayValue string `json:"dv,omitempty"`
}
type StreamEntryItem struct { type StreamEntryItem struct {
ID string `json:"id"` ID string `json:"id"`
Value map[string]any `json:"v"` Value map[string]any `json:"v"`

View File

@ -5,6 +5,7 @@ import (
) )
func containsBinary(str string) bool { func containsBinary(str string) bool {
//buf := []byte(str)
//size := 0 //size := 0
//for start := 0; start < len(buf); start += size { //for start := 0; start < len(buf); start += size {
// var r rune // var r rune
@ -14,9 +15,25 @@ func containsBinary(str string) bool {
//} //}
rs := []rune(str) rs := []rune(str)
for _, r := range rs { for _, r := range rs {
if !unicode.IsPrint(r) && r != '\n' { if !unicode.IsPrint(r) && !unicode.IsSpace(r) {
return true return true
} }
} }
return false return false
} }
func isSameChar(str string) bool {
if len(str) <= 0 {
return false
}
rs := []rune(str)
first := rs[0]
for _, r := range rs {
if r != first {
return false
}
}
return true
}

View File

@ -109,9 +109,11 @@ func autoDecode(str string) (value, resultDecode string) {
// pure digit content may incorrect regard as some encoded type, skip decode // pure digit content may incorrect regard as some encoded type, skip decode
if match, _ := regexp.MatchString(`^\d+$`, str); !match { if match, _ := regexp.MatchString(`^\d+$`, str); !match {
var ok bool var ok bool
if value, ok = decodeBase64(str); ok { if len(str)%4 == 0 && !isSameChar(str) {
resultDecode = types.DECODE_BASE64 if value, ok = decodeBase64(str); ok {
return resultDecode = types.DECODE_BASE64
return
}
} }
if value, ok = decodeGZip(str); ok { if value, ok = decodeGZip(str); ok {

View File

@ -158,7 +158,7 @@ const saveEdit = async (field, value, decode, format) => {
throw new Error('row not exists') throw new Error('row not exists')
} }
const { updated, success, msg } = await browserStore.setHash({ const { success, msg } = await browserStore.setHash({
server: props.name, server: props.name,
db: props.db, db: props.db,
key: keyName.value, key: keyName.value,
@ -167,16 +167,11 @@ const saveEdit = async (field, value, decode, format) => {
value, value,
decode, decode,
format, format,
retDecode: props.decode,
retFormat: props.format,
index: [currentEditRow.no - 1],
}) })
if (success) { if (success) {
row.k = field
row.v = updated[row.k] || ''
const { value: displayVal } = await browserStore.convertValue({
value: row.v,
decode: props.decode,
format: props.format,
})
row.dv = displayVal
$message.success(i18n.t('dialogue.save_value_succ')) $message.success(i18n.t('dialogue.save_value_succ'))
} else { } else {
$message.error(msg) $message.error(msg)

View File

@ -6,6 +6,7 @@ import {
isEmpty, isEmpty,
join, join,
last, last,
map,
remove, remove,
set, set,
size, size,
@ -1010,7 +1011,10 @@ const useBrowserStore = defineStore('browser', {
* @param {string} [value] * @param {string} [value]
* @param {string} [decode] * @param {string} [decode]
* @param {string} [format] * @param {string} [format]
* @param {string} [retDecode]
* @param {string} [retFormat]
* @param {boolean} [refresh] * @param {boolean} [refresh]
* @param {number} [index] index for retrieve affect entries quickly
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
*/ */
async setHash({ async setHash({
@ -1022,7 +1026,9 @@ const useBrowserStore = defineStore('browser', {
value = '', value = '',
decode = decodeTypes.NONE, decode = decodeTypes.NONE,
format = formatTypes.RAW, format = formatTypes.RAW,
refresh, retDecode,
retFormat,
index,
}) { }) {
try { try {
const { data, success, msg } = await SetHashValue({ const { data, success, msg } = await SetHashValue({
@ -1036,15 +1042,30 @@ const useBrowserStore = defineStore('browser', {
format, format,
}) })
if (success) { if (success) {
const { updated = {}, removed = [], replaced = {} } = data /**
if (refresh === true) { * @type {{updated: HashEntryItem[], removed: HashEntryItem[], updated: HashEntryItem[], replaced: HashReplaceItem[]}}
const tab = useTabStore() */
if (!isEmpty(removed)) { const { updated = [], removed = [], added = [], replaced = [] } = data
tab.removeValueEntries({ server, db, key, type: 'hash', entries: removed }) const tab = useTabStore()
} if (!isEmpty(removed)) {
if (!isEmpty(updated)) { const removedKeys = map(removed, (e) => e.k)
tab.upsertValueEntries({ server, db, key, type: 'hash', entries: updated }) tab.removeValueEntries({ server, db, key, type: 'hash', entries: removedKeys })
} }
if (!isEmpty(updated)) {
tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated })
}
if (!isEmpty(added)) {
tab.insertValueEntries({ server, db, key, type: 'hash', entries: added })
}
if (!isEmpty(replaced)) {
tab.replaceValueEntries({
server,
db,
key,
type: 'hash',
entries: replaced,
index: [index],
})
} }
return { success, updated } return { success, updated }
} else { } else {
@ -1071,7 +1092,7 @@ const useBrowserStore = defineStore('browser', {
const { updated = [], added = [] } = data const { updated = [], added = [] } = data
const tab = useTabStore() const tab = useTabStore()
if (!isEmpty(updated)) { if (!isEmpty(updated)) {
tab.replaceValueEntries({ server, db, key, type: 'hash', entries: updated }) tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated })
} }
if (!isEmpty(added)) { if (!isEmpty(added)) {
tab.insertValueEntries({ server, db, key, type: 'hash', entries: added }) tab.insertValueEntries({ server, db, key, type: 'hash', entries: added })
@ -1360,7 +1381,7 @@ const useBrowserStore = defineStore('browser', {
tab.insertValueEntries({ server, db, key, type: 'zset', entries: added }) tab.insertValueEntries({ server, db, key, type: 'zset', entries: added })
} }
if (!isEmpty(updated)) { if (!isEmpty(updated)) {
tab.replaceValueEntries({ server, db, key, type: 'zset', entries: updated }) tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated })
} }
return { success } return { success }
} else { } else {

View File

@ -1,4 +1,4 @@
import { assign, find, findIndex, get, indexOf, isEmpty, pullAt, remove, set, size } from 'lodash' import { assign, find, findIndex, get, includes, isEmpty, pullAt, remove, set, size } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
const useTabStore = defineStore('tab', { const useTabStore = defineStore('tab', {
@ -25,6 +25,62 @@ const useTabStore = defineStore('tab', {
* @param {boolean} [loading] * @param {boolean} [loading]
*/ */
/**
* @typedef {Object} ListEntryItem
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} ListReplaceItem
* @property {number} index
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} HashEntryItem
* @property {string} k field name
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} HashReplaceItem
* @property {string|number[]} k field name
* @property {string|number[]} nk new field name
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} SetEntryItem
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} ZSetEntryItem
* @property {number} s score
* @property {string|number[]} v value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} ZSetReplaceItem
* @property {number} s score
* @property {string|number[]} v value
* @property {string|number[]} nv new value
* @property {string} [dv] display value
*/
/**
* @typedef {Object} StreamEntryItem
* @property {string} id
* @property {Object.<string, *>} v value
* @property {string} [dv] display value
*/
/** /**
* *
* @returns {{tabList: TabItem[], activatedTab: string, activatedIndex: number}} * @returns {{tabList: TabItem[], activatedTab: string, activatedIndex: number}}
@ -177,151 +233,13 @@ const useTabStore = defineStore('tab', {
} }
}, },
/**
* update or insert value entries
* @param {string} server
* @param {number} db
* @param {string} key
* @param {string} type
* @param {string[]|Object.<string, number>|Object.<number, string>|{k:string, v:string}[]} entries
* @param {boolean} [prepend] for list only
* @param {boolean} [reset]
* @param {boolean} [nocheck] ignore conflict checking for hash/set/zset
*/
upsertValueEntries({ server, db, key, type, entries, prepend, reset, nocheck }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'list': // string[] | Object.<number, string>
if (entries instanceof Array) {
// append or prepend items
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
if (prepend === true) {
tab.value = [...entries, ...tab.value]
} else {
tab.value.push(...entries)
}
tab.length += size(entries)
}
} else {
// replace by index
tab.value = tab.value || []
for (const idx in entries) {
set(tab.value, idx, entries[idx])
}
}
break
case 'hash': // Object.<string, string>
if (reset === true) {
tab.value = {}
tab.length = 0
} else {
tab.value = tab.value || {}
}
if (entries instanceof Array) {
// append new item
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
tab.value.push(...entries)
}
tab.length += size(entries)
} else {
// replace item {key: value}
for (const ent in entries) {
let found = false
for (const elem of tab.value) {
if (elem.k === ent) {
elem.v = entries[elem.k]
elem.dv = ent.dv
found = true
break
}
}
if (!found && nocheck !== true) {
tab.length += 1
tab.value.push(ent)
}
}
}
break
case 'set': // string[] | Object.{string, string}
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
if (entries instanceof Array) {
// add items
for (const elem of entries) {
if (nocheck !== true && indexOf(tab.value, elem) === -1) {
tab.value.push(elem)
tab.length += 1
}
}
} else {
// replace items
for (const k in entries) {
const idx = indexOf(tab.value, k)
if (idx !== -1) {
tab.value[idx] = entries[k]
} else {
tab.value.push(entries[k])
tab.length += 1
}
}
}
}
break
case 'zset': // {value: string, score: number}
if (reset === true) {
tab.value = Object.entries(entries).map(([value, score]) => ({ value, score }))
} else {
tab.value = tab.value || []
for (const val in entries) {
if (nocheck !== true) {
const ent = find(tab.value, (e) => e.value === val)
if (ent != null) {
ent.score = entries[val]
} else {
tab.value.push({ value: val, score: entries[val] })
tab.length += 1
}
} else {
tab.value.push({ value: val, score: entries[val] })
tab.length += 1
}
}
}
break
case 'stream': // [{id: string, value: []any}]
if (reset === true) {
tab.value = entries
} else {
tab.value = tab.value || []
tab.value = [...entries, ...tab.value]
}
break
}
},
/** /**
* insert entries * insert entries
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string|number[]} key
* @param {string} type * @param {string} type
* @param {any[]} entries * @param {ListEntryItem[]|HashEntryItem[]|SetEntryItem[]|ZSetEntryItem[]|StreamEntryItem[]} entries
* @param {boolean} [prepend] for list only * @param {boolean} [prepend] for list only
*/ */
insertValueEntries({ server, db, key, type, entries, prepend }) { insertValueEntries({ server, db, key, type, entries, prepend }) {
@ -363,28 +281,15 @@ const useTabStore = defineStore('tab', {
* @param {number} db * @param {number} db
* @param {string|number[]} key * @param {string|number[]} key
* @param {string} type * @param {string} type
* @param {any[]} entries * @param {ListEntryItem[]|HashEntryItem[]|SetEntryItem[]|ZSetEntryItem[]|StreamEntryItem[]} entries
*/ */
replaceValueEntries({ server, db, key, type, entries }) { updateValueEntries({ server, db, key, type, entries }) {
const tab = find(this.tabList, { name: server, db, key }) const tab = find(this.tabList, { name: server, db, key })
if (tab == null) { if (tab == null) {
return return
} }
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'list': // {index:number, v:string, dv:[string]}[]
tab.value = tab.value || []
for (const entry of entries) {
if (size(tab.value) < entry.index) {
tab.value[entry.index] = entry
} else {
// out of range, append
tab.value.push(entry)
tab.length += 1
}
}
break
case 'hash': // {k:string, v:string, dv:string}[] case 'hash': // {k:string, v:string, dv:string}[]
tab.value = tab.value || [] tab.value = tab.value || []
for (const entry of entries) { for (const entry of entries) {
@ -423,6 +328,102 @@ const useTabStore = defineStore('tab', {
tab.length += 1 tab.length += 1
} }
} }
break
}
},
/**
* replace entry item key or field in value
* @param {string} server
* @param {number} db
* @param {string|number[]} key
* @param {string} type
* @param {ListReplaceItem[]|HashReplaceItem[]|ZSetReplaceItem[]} entries
* @param {number[]} [index] indexes for replacement, can improve search efficiency if configured
*/
replaceValueEntries({ server, db, key, type, entries, index }) {
const tab = find(this.tabList, { name: server, db, key })
if (tab == null) {
return
}
switch (type.toLowerCase()) {
case 'list': // {index:number, v:string, dv:[string]}[]
tab.value = tab.value || []
for (const entry of entries) {
if (size(tab.value) < entry.index) {
tab.value[entry.index] = entry
} else {
// out of range, append
tab.value.push(entry)
tab.length += 1
}
}
break
case 'hash':
tab.value = tab.value || []
for (const idx of index) {
const entry = get(tab.value, idx)
if (entry != null) {
/** @type HashReplaceItem[] **/
const replaceEntry = remove(entries, (e) => e.k === entry.k)
if (!isEmpty(replaceEntry)) {
entry.k = replaceEntry[0].nk
entry.v = replaceEntry[0].v
entry.dv = replaceEntry[0].dv
}
}
}
// the left entries do not included in index list, try to retrieve the whole list
for (const entry of entries) {
let updated = false
for (const val of tab.value) {
if (val.k === entry.k) {
val.k = entry.nk
val.v = entry.v
val.dv = entry.dv
updated = true
break
}
}
if (!updated) {
// no match element, append
tab.value.push({
k: entry.nk,
v: entry.v,
dv: entry.dv,
})
tab.length += 1
}
}
break
case 'zset':
tab.value = tab.value || []
for (const entry of entries) {
let updated = false
for (const val of tab.value) {
if (val.v === entry.v) {
val.s = entry.s
val.v = entry.nv
val.dv = entry.dv
updated = true
break
}
}
if (!updated) {
// no match element, append
tab.value.push({
s: entry.s,
v: entry.nv,
dv: entry.dv,
})
tab.length += 1
}
}
break
} }
}, },
@ -460,12 +461,8 @@ const useTabStore = defineStore('tab', {
case 'hash': // string[] case 'hash': // string[]
tab.value = tab.value || {} tab.value = tab.value || {}
for (const k of entries) { const removedElems = remove(tab.value, (e) => includes(entries, e.k))
if (tab.value.hasOwnProperty(k)) { tab.length -= size(removedElems)
delete tab.value[k]
tab.length -= 1
}
}
break break
case 'set': // []string case 'set': // []string

View File

@ -1,6 +1,6 @@
import usePreferencesStore from 'stores/preferences.js' import usePreferencesStore from 'stores/preferences.js'
import { createDiscreteApi, darkTheme } from 'naive-ui' import { createDiscreteApi, darkTheme } from 'naive-ui'
import { themeOverrides } from '@/utils/theme.js' import { darkThemeOverrides, themeOverrides } from '@/utils/theme.js'
import { i18nGlobal } from '@/utils/i18n.js' import { i18nGlobal } from '@/utils/i18n.js'
import { computed } from 'vue' import { computed } from 'vue'
@ -109,6 +109,7 @@ export async function setupDiscreteApi() {
containerStyle: { containerStyle: {
marginBottom: '38px', marginBottom: '38px',
}, },
themeOverrides: prefStore.isDark ? darkThemeOverrides.Message : themeOverrides.Message,
}, },
notificationProviderProps: { notificationProviderProps: {
max: 5, max: 5,