diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index 505d1f6..727c9f2 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -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 { - // update field value - _, err = client.HSet(ctx, key, param.Field, saveStr).Result() - updatedField[param.NewField] = saveStr + 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 + 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 } @@ -1314,8 +1348,7 @@ func (b *browserService) SetSetItem(server string, db int, k any, remove bool, m for _, member := range members { if affected, _ = client.SRem(ctx, key, member).Result(); affected > 0 { removed = append(removed, types.SetEntryItem{ - Value: member, - DisplayValue: "", // TODO: convert to display value + Value: member, }) } } diff --git a/backend/types/js_resp.go b/backend/types/js_resp.go index 16e3598..25c6f29 100644 --- a/backend/types/js_resp.go +++ b/backend/types/js_resp.go @@ -60,14 +60,16 @@ type SetListParam struct { } type SetHashParam struct { - Server string `json:"server"` - DB int `json:"db"` - Key any `json:"key"` - Field string `json:"field,omitempty"` - NewField string `json:"newField,omitempty"` - Value any `json:"value"` - Format string `json:"format,omitempty"` - Decode string `json:"decode,omitempty"` + Server string `json:"server"` + DB int `json:"db"` + Key any `json:"key"` + Field string `json:"field,omitempty"` + NewField string `json:"newField,omitempty"` + 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 { diff --git a/backend/types/redis_wrapper.go b/backend/types/redis_wrapper.go index 2c81a3d..2167211 100644 --- a/backend/types/redis_wrapper.go +++ b/backend/types/redis_wrapper.go @@ -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"` diff --git a/backend/utils/string/common.go b/backend/utils/string/common.go index 11ab00d..b26f44d 100644 --- a/backend/utils/string/common.go +++ b/backend/utils/string/common.go @@ -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 +} diff --git a/backend/utils/string/convert.go b/backend/utils/string/convert.go index da70bb6..e8c3e2f 100644 --- a/backend/utils/string/convert.go +++ b/backend/utils/string/convert.go @@ -109,9 +109,11 @@ 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 value, ok = decodeBase64(str); ok { - resultDecode = types.DECODE_BASE64 - return + if len(str)%4 == 0 && !isSameChar(str) { + if value, ok = decodeBase64(str); ok { + resultDecode = types.DECODE_BASE64 + return + } } if value, ok = decodeGZip(str); ok { diff --git a/frontend/src/components/content_value/ContentValueHash.vue b/frontend/src/components/content_value/ContentValueHash.vue index bdca71c..4709ccc 100644 --- a/frontend/src/components/content_value/ContentValueHash.vue +++ b/frontend/src/components/content_value/ContentValueHash.vue @@ -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) diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js index 1232c5c..e442edb 100644 --- a/frontend/src/stores/browser.js +++ b/frontend/src/stores/browser.js @@ -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) { - const tab = useTabStore() - if (!isEmpty(removed)) { - tab.removeValueEntries({ server, db, key, type: 'hash', entries: removed }) - } - if (!isEmpty(updated)) { - tab.upsertValueEntries({ server, db, key, type: 'hash', entries: updated }) - } + /** + * @type {{updated: HashEntryItem[], removed: HashEntryItem[], updated: HashEntryItem[], replaced: HashReplaceItem[]}} + */ + const { updated = [], removed = [], added = [], replaced = [] } = data + const tab = useTabStore() + if (!isEmpty(removed)) { + const removedKeys = map(removed, (e) => e.k) + 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 } } 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 { diff --git a/frontend/src/stores/tab.js b/frontend/src/stores/tab.js index caa82fe..0b17f97 100644 --- a/frontend/src/stores/tab.js +++ b/frontend/src/stores/tab.js @@ -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.} 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.|Object.|{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. - 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. - 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 diff --git a/frontend/src/utils/discrete.js b/frontend/src/utils/discrete.js index 461e502..122b8a6 100644 --- a/frontend/src/utils/discrete.js +++ b/frontend/src/utils/discrete.js @@ -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,