From 5b84fa59f6c88d47d76b34589a0d6412905ac892 Mon Sep 17 00:00:00 2001 From: Lykin <137850705+tiny-craft@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:15:02 +0800 Subject: [PATCH] perf: set value support format viewing #65 --- backend/services/browser_service.go | 81 +++--- backend/types/js_resp.go | 21 +- backend/types/redis_wrapper.go | 16 ++ backend/utils/string/convert.go | 53 ++-- .../content_value/ContentValueHash.vue | 1 - .../content_value/ContentValueList.vue | 14 +- .../content_value/ContentValueSet.vue | 231 ++++++++++++------ .../content_value/ContentValueWrapper.vue | 46 ++-- frontend/src/stores/browser.js | 25 +- frontend/src/utils/discrete.js | 3 + 10 files changed, 309 insertions(+), 182 deletions(-) diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index 07857a8..dd2aa82 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -580,6 +580,11 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS resp.Msg = "key not exists" return } + var doConvert bool + if (len(param.Decode) > 0 && param.Decode != types.DECODE_NONE) || + (len(param.Format) > 0 && param.Format != types.FORMAT_RAW) { + doConvert = true + } var data types.KeyDetail //var cursor uint64 @@ -636,14 +641,15 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS loadListHandle := func() ([]types.ListEntryItem, bool, error) { var loadVal []string var cursor uint64 + var subErr error if param.Full { // load all cursor = 0 - loadVal, err = client.LRange(ctx, key, 0, -1).Result() + loadVal, subErr = client.LRange(ctx, key, 0, -1).Result() } else { cursor, _ = getEntryCursor() scanSize := int64(Preferences().GetScanSize()) - loadVal, err = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result() + loadVal, subErr = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result() cursor = cursor + uint64(scanSize) if len(loadVal) < int(scanSize) { cursor = 0 @@ -652,12 +658,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS setEntryCursor(cursor) items := make([]types.ListEntryItem, len(loadVal)) - doConvert := len(param.Decode) > 0 && len(param.Format) > 0 for i, val := range loadVal { - items[i] = types.ListEntryItem{ - Value: val, - DisplayValue: "", - } items[i].Value = val if doConvert { if dv, _, _ := strutil.ConvertTo(val, param.Decode, param.Format); dv != val { @@ -665,8 +666,8 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } } - if err != nil { - return items, false, err + if subErr != nil { + return items, false, subErr } return items, cursor == 0, nil } @@ -685,14 +686,14 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS items := make([]types.HashEntryItem, 0, scanSize) var loadedVal []string var cursor uint64 - doConvert := len(param.Decode) > 0 && len(param.Format) > 0 + var subErr error if param.Full { // load all cursor = 0 for { - loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", scanSize).Result() - if err != nil { - return nil, false, err + loadedVal, cursor, subErr = client.HScan(ctx, key, cursor, "*", scanSize).Result() + if subErr != nil { + return nil, false, subErr } for i := 0; i < len(loadedVal); i += 2 { items = append(items, types.HashEntryItem{ @@ -711,9 +712,9 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } else { cursor, _ = getEntryCursor() - loadedVal, cursor, err = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result() - if err != nil { - return nil, false, err + loadedVal, cursor, subErr = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result() + if subErr != nil { + return nil, false, subErr } for i := 0; i < len(loadedVal); i += 2 { items = append(items, types.HashEntryItem{ @@ -739,30 +740,39 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } case "set": - loadSetHandle := func() ([]string, bool, error) { - var items []string + loadSetHandle := func() ([]types.SetEntryItem, bool, error) { + var items []types.SetEntryItem var cursor uint64 + var subErr error scanSize := int64(Preferences().GetScanSize()) var loadedKey []string if param.Full { // load all cursor = 0 for { - loadedKey, cursor, err = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() - if err != nil { - return items, false, err + loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() + if subErr != nil { + return items, false, subErr } - items = append(items, loadedKey...) if cursor == 0 { break } } } else { cursor, _ = getEntryCursor() - loadedKey, cursor, err = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() - items = append(items, loadedKey...) + loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() } setEntryCursor(cursor) + + items = make([]types.SetEntryItem, len(loadedKey)) + for i, val := range loadedKey { + items[i].Value = val + if doConvert { + if dv, _, _ := strutil.ConvertTo(val, param.Decode, param.Format); dv != val { + items[i].DisplayValue = dv + } + } + } return items, cursor == 0, nil } @@ -1245,23 +1255,36 @@ func (b *browserService) SetSetItem(connName string, db int, k any, remove bool, } // UpdateSetItem replace member of set -func (b *browserService) UpdateSetItem(connName string, db int, k any, value, newValue string) (resp types.JSResp) { - item, err := b.getRedisClient(connName, db) +func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp types.JSResp) { + item, err := b.getRedisClient(param.Server, param.DB) if err != nil { resp.Msg = err.Error() return } client, ctx := item.client, item.ctx - key := strutil.DecodeRedisKey(k) - _, _ = client.SRem(ctx, key, value).Result() - _, err = client.SAdd(ctx, key, newValue).Result() + key := strutil.DecodeRedisKey(param.Key) + // remove old value + str := strutil.DecodeRedisKey(param.Value) + _, _ = client.SRem(ctx, key, str).Result() + + // insert new value + str = strutil.DecodeRedisKey(param.NewValue) + var saveStr 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 + } + _, err = client.SAdd(ctx, key, saveStr).Result() if err != nil { resp.Msg = err.Error() return } resp.Success = true + resp.Data = map[string]any{ + "added": saveStr, + } return } diff --git a/backend/types/js_resp.go b/backend/types/js_resp.go index 7559fbd..89b5fa1 100644 --- a/backend/types/js_resp.go +++ b/backend/types/js_resp.go @@ -30,17 +30,6 @@ type KeyDetailParam struct { Full bool `json:"full"` } -type ListEntryItem struct { - 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 KeyDetail struct { Value any `json:"value"` Length int64 `json:"length,omitempty"` @@ -80,3 +69,13 @@ type SetHashParam struct { Format string `json:"format,omitempty"` Decode string `json:"decode,omitempty"` } + +type SetSetParam struct { + Server string `json:"server"` + DB int `json:"db"` + Key any `json:"key"` + Value any `json:"value"` + NewValue any `json:"newValue"` + Format string `json:"format,omitempty"` + Decode string `json:"decode,omitempty"` +} diff --git a/backend/types/redis_wrapper.go b/backend/types/redis_wrapper.go index 1419136..f830aab 100644 --- a/backend/types/redis_wrapper.go +++ b/backend/types/redis_wrapper.go @@ -1,5 +1,21 @@ package types +type ListEntryItem struct { + 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 SetEntryItem struct { + Value any `json:"v"` + DisplayValue string `json:"dv,omitempty"` +} + type ZSetItem struct { Value string `json:"value"` Score float64 `json:"score"` diff --git a/backend/utils/string/convert.go b/backend/utils/string/convert.go index 67fa749..8b82e53 100644 --- a/backend/utils/string/convert.go +++ b/backend/utils/string/convert.go @@ -106,31 +106,34 @@ func decodeWith(str, decodeType string) (value, resultDecode string) { // if no decode is possible, it will return the origin string value and "none" decode type func autoDecode(str string) (value, resultDecode string) { if len(str) > 0 { - var ok bool - if value, ok = decodeBase64(str); ok { - resultDecode = types.DECODE_BASE64 - return - } + // 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 value, ok = decodeGZip(str); ok { - resultDecode = types.DECODE_GZIP - return - } + if value, ok = decodeGZip(str); ok { + resultDecode = types.DECODE_GZIP + return + } - // FIXME: skip decompress with deflate due to incorrect format checking - //if value, ok = decodeDeflate(str); ok { - // resultDecode = types.DECODE_DEFLATE - // return - //} + // FIXME: skip decompress with deflate due to incorrect format checking + //if value, ok = decodeDeflate(str); ok { + // resultDecode = types.DECODE_DEFLATE + // return + //} - if value, ok = decodeZStd(str); ok { - resultDecode = types.DECODE_ZSTD - return - } + if value, ok = decodeZStd(str); ok { + resultDecode = types.DECODE_ZSTD + return + } - if value, ok = decodeBrotli(str); ok { - resultDecode = types.DECODE_BROTLI - return + if value, ok = decodeBrotli(str); ok { + resultDecode = types.DECODE_BROTLI + return + } } } @@ -215,11 +218,9 @@ func decodeJson(str string) (string, bool) { } func decodeBase64(str string) (string, bool) { - if match, _ := regexp.MatchString(`^\d+$`, str); !match { - if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil { - if s := string(decodedStr); !containsBinary(s) { - return s, true - } + if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil { + if s := string(decodedStr); !containsBinary(s) { + return s, true } } return str, false diff --git a/frontend/src/components/content_value/ContentValueHash.vue b/frontend/src/components/content_value/ContentValueHash.vue index 320924c..94fab1e 100644 --- a/frontend/src/components/content_value/ContentValueHash.vue +++ b/frontend/src/components/content_value/ContentValueHash.vue @@ -373,7 +373,6 @@ defineExpose({
{ // if (!isEmpty(row.dv)) { - // console.log(row.dv) // return h(NCode, { language: 'json', wordWrap: true, code: row.dv }) // } return row.dv || row.v @@ -96,6 +95,14 @@ const startEdit = async (no, value) => { currentEditRow.no = no } +/** + * + * @param {string|number} pos + * @param {string} value + * @param {string} decode + * @param {string} format + * @return {Promise} + */ const saveEdit = async (pos, value, decode, format) => { try { const index = parseInt(pos) - 1 @@ -149,8 +156,6 @@ const actionColumn = { editing: false, bindKey: `#${index + 1}`, onEdit: () => { - currentEditRow.no = index + 1 - currentEditRow.value = row startEdit(index + 1, row.v) }, onDelete: async () => { @@ -162,7 +167,7 @@ const actionColumn = { index, ) if (success) { - $message.success(i18n.t('dialogue.delete_key_succ', { key: '#' + row.no })) + $message.success(i18n.t('dialogue.delete_key_succ', { key: `#${index + 1}` })) } else { $message.error(msg) } @@ -307,7 +312,6 @@ defineExpose({
{ const browserStore = useBrowserStore() const dialogStore = useDialogStore() const keyType = redisTypes.SET -const currentEditRow = ref({ +const currentEditRow = reactive({ no: 0, value: null, + format: formatTypes.RAW, + decode: decodeTypes.NONE, +}) +const inEdit = computed(() => { + return currentEditRow.no > 0 }) - const valueColumn = reactive({ key: 'value', title: i18n.t('common.value'), align: 'center', titleAlign: 'center', + ellipsis: { + tooltip: true, + }, filterOptionValue: null, - filter(value, row) { - return !!~row.value.indexOf(value.toString()) + filter: (value, row) => { + return !!~row.v.indexOf(value.toString()) }, render: (row) => { - const isEdit = currentEditRow.value.no === row.no - if (isEdit) { - return h(NInput, { - value: currentEditRow.value.value, - type: 'textarea', - autosize: { minRow: 2, maxRows: 5 }, - style: 'text-align: left;', - 'onUpdate:value': (val) => { - currentEditRow.value.value = val - }, - }) - } else { - return h(NCode, { language: 'plaintext', wordWrap: true }, { default: () => row.value }) - } + // if (!isEmpty(row.dv)) { + // return h(NCode, { language: 'json', wordWrap: true, code: row.dv }) + // } + return row.dv || row.v }, }) -const cancelEdit = () => { - currentEditRow.value.no = 0 +const startEdit = async (no, value) => { + currentEditRow.value = value + currentEditRow.no = no +} + +/** + * + * @param {string|number} pos + * @param {string} value + * @param {string} decode + * @param {string} format + * @return {Promise} + */ +const saveEdit = async (pos, value, decode, format) => { + try { + const index = parseInt(pos) - 1 + const row = props.value[index] + if (row == null) { + throw new Error('row not exists') + } + + const { added, success, msg } = await browserStore.updateSetItem({ + server: props.name, + db: props.db, + key: keyName.value, + value: row.v, + newValue: value, + decode, + format, + }) + if (success) { + row.v = added + 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) + } + } catch (e) { + $message.error(e.message) + } finally { + resetEdit() + } +} + +const resetEdit = () => { + currentEditRow.no = 0 + currentEditRow.value = null } const actionColumn = { @@ -101,14 +151,12 @@ const actionColumn = { align: 'center', titleAlign: 'center', fixed: 'right', - render: (row) => { + render: (row, index) => { return h(EditableTableColumn, { - editing: currentEditRow.value.no === row.no, - bindKey: row.value, + editing: false, + bindKey: `#${index + 1}`, onEdit: () => { - currentEditRow.value.no = row.no - currentEditRow.value.key = row.key - currentEditRow.value.value = row.value + startEdit(index + 1, row.v) }, onDelete: async () => { try { @@ -116,10 +164,10 @@ const actionColumn = { props.name, props.db, keyName.value, - row.value, + row.v, ) if (success) { - $message.success(i18n.t('dialogue.delete_key_succ', { key: row.value })) + $message.success(i18n.t('dialogue.delete_key_succ', { key: row.v })) } else { $message.error(msg) } @@ -127,58 +175,61 @@ const actionColumn = { $message.error(e.message) } }, - onSave: async () => { - try { - const { success, msg } = await browserStore.updateSetItem( - props.name, - props.db, - keyName.value, - row.value, - currentEditRow.value.value, - ) - if (success) { - $message.success(i18n.t('dialogue.save_value_succ')) - } else { - $message.error(msg) - } - } catch (e) { - $message.error(e.message) - } finally { - currentEditRow.value.no = 0 - } - }, - onCancel: cancelEdit, }) }, } + const columns = computed(() => { - return [ - { - key: 'no', - title: '#', - width: 80, - align: 'center', - titleAlign: 'center', - }, - valueColumn, - actionColumn, - ] + if (!inEdit.value) { + return [ + { + key: 'no', + title: '#', + width: 80, + align: 'center', + titleAlign: 'center', + render: (row, index) => { + return index + 1 + }, + }, + valueColumn, + actionColumn, + ] + } else { + return [ + { + key: 'no', + title: '#', + width: 80, + align: 'center', + titleAlign: 'center', + render: (row, index) => { + if (index + 1 === currentEditRow.no) { + // editing row, show edit state + return h(NIcon, { size: 16, color: 'red' }, () => h(Edit, { strokeWidth: 5 })) + } else { + return index + 1 + } + }, + }, + valueColumn, + ] + } }) -const tableData = computed(() => { - const data = [] - const len = size(props.value) - for (let i = 0; i < len; i++) { - data.push({ - no: i + 1, - value: props.value[i], - }) +const rowProps = (row, index) => { + return { + onClick: () => { + // in edit mode, switch edit row by click + if (inEdit.value) { + startEdit(index + 1, row.v) + } + }, } - return data -}) +} const entries = computed(() => { - const len = size(tableData.value) + const len = size(props.value) return `${len} / ${Math.max(len, props.length)}` }) @@ -199,10 +250,14 @@ const onUpdateFilter = (filters, sourceColumn) => { valueColumn.filterOptionValue = filters[sourceColumn.key] } +const onFormatChanged = (selDecode, selFormat) => { + emit('reload', selDecode, selFormat) +} + defineExpose({ reset: () => { clearFilter() - cancelEdit() + resetEdit() }, }) @@ -254,14 +309,15 @@ defineExpose({ {{ $t('interface.add_row') }}
-
+
+ + + +
diff --git a/frontend/src/components/content_value/ContentValueWrapper.vue b/frontend/src/components/content_value/ContentValueWrapper.vue index 6aeb5a7..5a60519 100644 --- a/frontend/src/components/content_value/ContentValueWrapper.vue +++ b/frontend/src/components/content_value/ContentValueWrapper.vue @@ -163,28 +163,30 @@ watch(() => data.value?.keyPath, initContent) {{ $t('interface.reload') }} - - - + + + + diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js index 371c2e9..3cebaa6 100644 --- a/frontend/src/stores/browser.js +++ b/frontend/src/stores/browser.js @@ -1200,7 +1200,7 @@ const useBrowserStore = defineStore('browser', { * @param {string} format * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} */ - async updateListItem({ server, db, key, index, value, decode, format }) { + async updateListItem({ server, db, key, index, value, decode = decodeTypes.NONE, format = formatTypes.RAW }) { try { const { data, success, msg } = await SetListItem({ server, db, key, index, value, decode, format }) if (success) { @@ -1266,7 +1266,7 @@ const useBrowserStore = defineStore('browser', { */ async addSetItem(connName, db, key, value) { try { - if (!value instanceof Array) { + if ((!value) instanceof Array) { value = [value] } const { data, success, msg } = await SetSetItem(connName, db, key, false, value) @@ -1284,20 +1284,23 @@ const useBrowserStore = defineStore('browser', { /** * update value of set item - * @param {string} connName + * @param {string} server * @param {number} db * @param {string|number[]} key - * @param {string} value - * @param {string} newValue - * @returns {Promise<{[msg]: string, success: boolean}>} + * @param {string|number[]} value + * @param {string|number[]} newValue + * @param {string} [decode] + * @param {string} [format] + * @returns {Promise<{[msg]: string, success: boolean, [added]: string|number[]}>} */ - async updateSetItem(connName, db, key, value, newValue) { + async updateSetItem({ server, db, key, value, newValue, decode = decodeTypes.NONE, format = formatTypes.RAW }) { try { - const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue) + const { data, success, msg } = await UpdateSetItem({ server, db, key, value, newValue, decode, format }) if (success) { - const tab = useTabStore() - tab.upsertValueEntries({ server: connName, db, key, type: 'set', entries: { [value]: newValue } }) - return { success: true } + const { added } = data + // const tab = useTabStore() + // tab.upsertValueEntries({ server, db, key, type: 'set', entries: { [value]: newValue } }) + return { success: true, added } } else { return { success, msg } } diff --git a/frontend/src/utils/discrete.js b/frontend/src/utils/discrete.js index ab47c4e..29afc31 100644 --- a/frontend/src/utils/discrete.js +++ b/frontend/src/utils/discrete.js @@ -111,6 +111,9 @@ export async function setupDiscreteApi() { max: 5, placement: 'bottom-right', keepAliveOnHover: true, + containerStyle: { + marginBottom: '38px', + }, }, })