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

View File

@ -68,6 +68,8 @@ type SetHashParam struct {
Value any `json:"value"`
Format string `json:"format,omitempty"`
Decode string `json:"decode,omitempty"`
RetFormat string `json:"retFormat,omitempty"`
RetDecode string `json:"retDecode,omitempty"`
}
type SetSetParam struct {

View File

@ -5,12 +5,25 @@ type ListEntryItem struct {
DisplayValue string `json:"dv,omitempty"`
}
type ListReplaceItem struct {
Index int64 `json:"index"`
Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"`
}
type HashEntryItem struct {
Key string `json:"k"`
Value any `json:"v"`
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 {
Value any `json:"v"`
DisplayValue string `json:"dv,omitempty"`
@ -22,6 +35,13 @@ type ZSetEntryItem struct {
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 {
ID string `json:"id"`
Value map[string]any `json:"v"`

View File

@ -5,6 +5,7 @@ import (
)
func containsBinary(str string) bool {
//buf := []byte(str)
//size := 0
//for start := 0; start < len(buf); start += size {
// var r rune
@ -14,9 +15,25 @@ func containsBinary(str string) bool {
//}
rs := []rune(str)
for _, r := range rs {
if !unicode.IsPrint(r) && r != '\n' {
if !unicode.IsPrint(r) && !unicode.IsSpace(r) {
return true
}
}
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,10 +109,12 @@ func autoDecode(str string) (value, resultDecode string) {
// pure digit content may incorrect regard as some encoded type, skip decode
if match, _ := regexp.MatchString(`^\d+$`, str); !match {
var ok bool
if len(str)%4 == 0 && !isSameChar(str) {
if value, ok = decodeBase64(str); ok {
resultDecode = types.DECODE_BASE64
return
}
}
if value, ok = decodeGZip(str); ok {
resultDecode = types.DECODE_GZIP

View File

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

View File

@ -6,6 +6,7 @@ import {
isEmpty,
join,
last,
map,
remove,
set,
size,
@ -1010,7 +1011,10 @@ const useBrowserStore = defineStore('browser', {
* @param {string} [value]
* @param {string} [decode]
* @param {string} [format]
* @param {string} [retDecode]
* @param {string} [retFormat]
* @param {boolean} [refresh]
* @param {number} [index] index for retrieve affect entries quickly
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
*/
async setHash({
@ -1022,7 +1026,9 @@ const useBrowserStore = defineStore('browser', {
value = '',
decode = decodeTypes.NONE,
format = formatTypes.RAW,
refresh,
retDecode,
retFormat,
index,
}) {
try {
const { data, success, msg } = await SetHashValue({
@ -1036,15 +1042,30 @@ const useBrowserStore = defineStore('browser', {
format,
})
if (success) {
const { updated = {}, removed = [], replaced = {} } = data
if (refresh === true) {
/**
* @type {{updated: HashEntryItem[], removed: HashEntryItem[], updated: HashEntryItem[], replaced: HashReplaceItem[]}}
*/
const { updated = [], removed = [], added = [], replaced = [] } = data
const tab = useTabStore()
if (!isEmpty(removed)) {
tab.removeValueEntries({ server, db, key, type: 'hash', entries: removed })
const removedKeys = map(removed, (e) => e.k)
tab.removeValueEntries({ server, db, key, type: 'hash', entries: removedKeys })
}
if (!isEmpty(updated)) {
tab.upsertValueEntries({ server, db, key, type: 'hash', entries: 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 }
} else {
@ -1071,7 +1092,7 @@ const useBrowserStore = defineStore('browser', {
const { updated = [], added = [] } = data
const tab = useTabStore()
if (!isEmpty(updated)) {
tab.replaceValueEntries({ server, db, key, type: 'hash', entries: updated })
tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated })
}
if (!isEmpty(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 })
}
if (!isEmpty(updated)) {
tab.replaceValueEntries({ server, db, key, type: 'zset', entries: updated })
tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated })
}
return { success }
} 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'
const useTabStore = defineStore('tab', {
@ -25,6 +25,62 @@ const useTabStore = defineStore('tab', {
* @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}}
@ -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
* @param {string} server
* @param {number} db
* @param {string|number[]} key
* @param {string} type
* @param {any[]} entries
* @param {ListEntryItem[]|HashEntryItem[]|SetEntryItem[]|ZSetEntryItem[]|StreamEntryItem[]} entries
* @param {boolean} [prepend] for list only
*/
insertValueEntries({ server, db, key, type, entries, prepend }) {
@ -363,28 +281,15 @@ const useTabStore = defineStore('tab', {
* @param {number} db
* @param {string|number[]} key
* @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 })
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': // {k:string, v:string, dv:string}[]
tab.value = tab.value || []
for (const entry of entries) {
@ -423,6 +328,102 @@ const useTabStore = defineStore('tab', {
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[]
tab.value = tab.value || {}
for (const k of entries) {
if (tab.value.hasOwnProperty(k)) {
delete tab.value[k]
tab.length -= 1
}
}
const removedElems = remove(tab.value, (e) => includes(entries, e.k))
tab.length -= size(removedElems)
break
case 'set': // []string

View File

@ -1,6 +1,6 @@
import usePreferencesStore from 'stores/preferences.js'
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 { computed } from 'vue'
@ -109,6 +109,7 @@ export async function setupDiscreteApi() {
containerStyle: {
marginBottom: '38px',
},
themeOverrides: prefStore.isDark ? darkThemeOverrides.Message : themeOverrides.Message,
},
notificationProviderProps: {
max: 5,