From a49d618288466773f898e5097ae967cae2bfab17 Mon Sep 17 00:00:00 2001 From: tiny-craft <137850705+tiny-craft@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:28:13 +0800 Subject: [PATCH] feat: add decode and format selection for Hash edit perf: move decode and format state management into the component internally perf: add component FormatSelector and ContentEntryEditor refactor: modified functions with an excessive number of parameters to accept an object as a parameter --- backend/services/browser_service.go | 237 +++++------------- backend/types/js_resp.go | 22 ++ .../DropdownSelector.vue | 9 +- frontend/src/components/common/IconButton.vue | 3 + .../src/components/content/ContentLogPane.vue | 2 +- .../content_value/ContentEntryEditor.vue | 180 +++++++++++++ .../content_value/ContentValueHash.vue | 214 ++++++++++------ .../content_value/ContentValueString.vue | 114 ++++----- .../content_value/ContentValueWrapper.vue | 37 +-- .../content_value/FormatSelector.vue | 60 +++++ .../src/components/dialogs/NewKeyDialog.vue | 9 +- frontend/src/langs/en-us.json | 1 + frontend/src/langs/pt-br.json | 1 + frontend/src/langs/zh-cn.json | 1 + frontend/src/stores/browser.js | 107 ++++++-- frontend/src/utils/theme.js | 4 + 16 files changed, 637 insertions(+), 364 deletions(-) rename frontend/src/components/{content_value => common}/DropdownSelector.vue (91%) create mode 100644 frontend/src/components/content_value/ContentEntryEditor.vue create mode 100644 frontend/src/components/content_value/FormatSelector.vue diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index 22b2384..737dd8b 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -629,7 +629,8 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS case "string": var str string str, err = client.Get(ctx, key).Result() - data.Value, data.DecodeType, data.ViewAs = strutil.ConvertTo(str, param.DecodeType, param.ViewAs) + data.Value = strutil.EncodeRedisKey(str) + //data.Value, data.DecodeType, data.ViewAs = strutil.ConvertTo(str, param.DecodeType, param.ViewAs) case "list": loadListHandle := func() ([]string, bool, error) { @@ -839,184 +840,56 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS return } -// GetKeyValue get value by key -func (b *browserService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) { - item, err := b.getRedisClient(connName, db) - if err != nil { - resp.Msg = err.Error() - return - } - - client, ctx := item.client, item.ctx - key := strutil.DecodeRedisKey(k) - var keyType string - var dur time.Duration - keyType, err = client.Type(ctx, key).Result() - if err != nil { - resp.Msg = err.Error() - return - } - - if keyType == "none" { - resp.Msg = "key not exists" - return - } - - var ttl int64 - if dur, err = client.TTL(ctx, key).Result(); err != nil { - ttl = -1 - } else { - if dur < 0 { - ttl = -1 - } else { - ttl = int64(dur.Seconds()) - } - } - - var value any - var size, length int64 - var cursor uint64 - switch strings.ToLower(keyType) { - case "string": - var str string - str, err = client.Get(ctx, key).Result() - value, decodeType, viewAs = strutil.ConvertTo(str, decodeType, viewAs) - length, _ = client.StrLen(ctx, key).Result() - size, _ = client.MemoryUsage(ctx, key, 0).Result() - case "list": - value, err = client.LRange(ctx, key, 0, -1).Result() - length, _ = client.LLen(ctx, key).Result() - size, _ = client.MemoryUsage(ctx, key, 0).Result() - case "hash": - //value, err = client.HGetAll(ctx, key).Result() - items := map[string]string{} - scanSize := int64(Preferences().GetScanSize()) - for { - var loadedVal []string - loadedVal, cursor, err = client.HScan(ctx, key, cursor, "*", scanSize).Result() - if err != nil { - resp.Msg = err.Error() - return - } - for i := 0; i < len(loadedVal); i += 2 { - items[loadedVal[i]] = loadedVal[i+1] - } - if cursor == 0 { - break - } - } - value = items - length, _ = client.HLen(ctx, key).Result() - size, _ = client.MemoryUsage(ctx, key, 0).Result() - case "set": - //value, err = client.SMembers(ctx, key).Result() - items := []string{} - scanSize := int64(Preferences().GetScanSize()) - for { - var loadedKey []string - loadedKey, cursor, err = client.SScan(ctx, key, cursor, "*", scanSize).Result() - if err != nil { - resp.Msg = err.Error() - return - } - items = append(items, loadedKey...) - if cursor == 0 { - break - } - } - value = items - length, _ = client.SCard(ctx, key).Result() - size, _ = client.MemoryUsage(ctx, key, 0).Result() - case "zset": - //value, err = client.ZRangeWithScores(ctx, key, 0, -1).Result() - var items []types.ZSetItem - scanSize := int64(Preferences().GetScanSize()) - for { - var loadedVal []string - loadedVal, cursor, err = client.ZScan(ctx, key, cursor, "*", scanSize).Result() - if err != nil { - resp.Msg = err.Error() - return - } - var score float64 - for i := 0; i < len(loadedVal); i += 2 { - if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil { - items = append(items, types.ZSetItem{ - Value: loadedVal[i], - Score: score, - }) - } - } - if cursor == 0 { - break - } - } - value = items - length, _ = client.ZCard(ctx, key).Result() - size, _ = client.MemoryUsage(ctx, key, 0).Result() - case "stream": - var msgs []redis.XMessage - items := []types.StreamItem{} - msgs, err = client.XRevRange(ctx, key, "+", "-").Result() - if err != nil { - resp.Msg = err.Error() - return - } - for _, msg := range msgs { - items = append(items, types.StreamItem{ - ID: msg.ID, - Value: msg.Values, - }) - } - value = items - length, _ = client.XLen(ctx, key).Result() - size, _ = client.MemoryUsage(ctx, key, 0).Result() - } - if err != nil { - resp.Msg = err.Error() - return - } +// ConvertValue convert value with decode method and format +// blank decodeType indicate auto decode +// blank viewAs indicate auto format +func (b *browserService) ConvertValue(value any, decode, format string) (resp types.JSResp) { + str := strutil.DecodeRedisKey(value) + value, decode, format = strutil.ConvertTo(str, decode, format) resp.Success = true resp.Data = map[string]any{ - "type": keyType, - "ttl": ttl, "value": value, - "size": size, - "length": length, - "viewAs": viewAs, - "decode": decodeType, + "decode": decode, + "format": format, } return } // SetKeyValue set value by key // @param ttl <= 0 means keep current ttl -func (b *browserService) SetKeyValue(connName string, db int, k any, keyType string, value any, ttl int64, viewAs, decode string) (resp types.JSResp) { - item, err := b.getRedisClient(connName, db) +func (b *browserService) SetKeyValue(param types.SetKeyParam) (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) + key := strutil.DecodeRedisKey(param.Key) var expiration time.Duration - if ttl < 0 { + if param.TTL < 0 { if expiration, err = client.PTTL(ctx, key).Result(); err != nil { expiration = redis.KeepTTL } } else { - expiration = time.Duration(ttl) * time.Second + expiration = time.Duration(param.TTL) * time.Second } - switch strings.ToLower(keyType) { + // use default decode type and format + if len(param.Decode) <= 0 { + param.Decode = types.DECODE_NONE + } + if len(param.Format) <= 0 { + param.Format = types.VIEWAS_PLAIN_TEXT + } + switch strings.ToLower(param.KeyType) { case "string": - if str, ok := value.(string); !ok { + if str, ok := param.Value.(string); !ok { resp.Msg = "invalid string value" return } else { var saveStr string - if saveStr, err = strutil.SaveAs(str, viewAs, decode); err != nil { - resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error()) + if saveStr, err = strutil.SaveAs(str, param.Format, param.Decode); err != nil { + resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, param.Format, err.Error()) return } _, err = client.Set(ctx, key, saveStr, 0).Result() @@ -1026,7 +899,7 @@ func (b *browserService) SetKeyValue(connName string, db int, k any, keyType str } } case "list": - if strs, ok := value.([]any); !ok { + if strs, ok := param.Value.([]any); !ok { resp.Msg = "invalid list value" return } else { @@ -1036,7 +909,7 @@ func (b *browserService) SetKeyValue(connName string, db int, k any, keyType str } } case "hash": - if strs, ok := value.([]any); !ok { + if strs, ok := param.Value.([]any); !ok { resp.Msg = "invalid hash value" return } else { @@ -1054,7 +927,7 @@ func (b *browserService) SetKeyValue(connName string, db int, k any, keyType str } } case "set": - if strs, ok := value.([]any); !ok || len(strs) <= 0 { + if strs, ok := param.Value.([]any); !ok || len(strs) <= 0 { resp.Msg = "invalid set value" return } else { @@ -1066,7 +939,7 @@ func (b *browserService) SetKeyValue(connName string, db int, k any, keyType str } } case "zset": - if strs, ok := value.([]any); !ok || len(strs) <= 0 { + if strs, ok := param.Value.([]any); !ok || len(strs) <= 0 { resp.Msg = "invalid zset value" return } else { @@ -1086,7 +959,7 @@ func (b *browserService) SetKeyValue(connName string, db int, k any, keyType str } } case "stream": - if strs, ok := value.([]any); !ok { + if strs, ok := param.Value.([]any); !ok { resp.Msg = "invalid stream value" return } else { @@ -1109,46 +982,52 @@ func (b *browserService) SetKeyValue(connName string, db int, k any, keyType str } resp.Success = true resp.Data = map[string]any{ - "value": value, + "value": param.Value, } return } // SetHashValue set hash field -func (b *browserService) SetHashValue(connName string, db int, k any, field, newField, value string) (resp types.JSResp) { - item, err := b.getRedisClient(connName, db) +func (b *browserService) SetHashValue(param types.SetHashParam) (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) + key := strutil.DecodeRedisKey(param.Key) + str := strutil.DecodeRedisKey(param.Value) + var saveStr string + if saveStr, err = strutil.SaveAs(str, param.Format, param.Decode); err != nil { + resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, param.Format, err.Error()) + return + } var removedField []string - updatedField := map[string]string{} - replacedField := map[string]string{} - if len(field) <= 0 { + 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, newField, value).Result() - updatedField[newField] = value - } else if len(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 - _, err = client.HDel(ctx, key, field, value).Result() - removedField = append(removedField, field) - } else if field == newField { - // replace field - _, err = client.HSet(ctx, key, newField, value).Result() - updatedField[newField] = value + _, 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 } else { // remove old field and add new field - if _, err = client.HDel(ctx, key, field).Result(); err != nil { + if _, err = client.HDel(ctx, key, param.Field).Result(); err != nil { resp.Msg = err.Error() return } - _, err = client.HSet(ctx, key, newField, value).Result() - removedField = append(removedField, field) - updatedField[newField] = value - replacedField[field] = newField + _, err = client.HSet(ctx, key, param.NewField, saveStr).Result() + removedField = append(removedField, param.Field) + updatedField[param.NewField] = saveStr + replacedField[param.Field] = param.NewField } if err != nil { resp.Msg = err.Error() diff --git a/backend/types/js_resp.go b/backend/types/js_resp.go index 06d240d..7b54681 100644 --- a/backend/types/js_resp.go +++ b/backend/types/js_resp.go @@ -37,3 +37,25 @@ type KeyDetail struct { DecodeType string `json:"decodeType,omitempty"` End bool `json:"end"` } + +type SetKeyParam struct { + Server string `json:"server"` + DB int `json:"db"` + Key any `json:"key"` + KeyType string `json:"keyType"` + Value any `json:"value"` + TTL int64 `json:"ttl"` + Format string `json:"format,omitempty"` + Decode string `json:"decode,omitempty"` +} + +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"` +} diff --git a/frontend/src/components/content_value/DropdownSelector.vue b/frontend/src/components/common/DropdownSelector.vue similarity index 91% rename from frontend/src/components/content_value/DropdownSelector.vue rename to frontend/src/components/common/DropdownSelector.vue index 00d0dae..5b3a9e5 100644 --- a/frontend/src/components/content_value/DropdownSelector.vue +++ b/frontend/src/components/common/DropdownSelector.vue @@ -1,6 +1,6 @@ + + + + diff --git a/frontend/src/components/content_value/ContentValueHash.vue b/frontend/src/components/content_value/ContentValueHash.vue index aec8370..0d75b50 100644 --- a/frontend/src/components/content_value/ContentValueHash.vue +++ b/frontend/src/components/content_value/ContentValueHash.vue @@ -3,7 +3,7 @@ import { computed, h, reactive, ref } from 'vue' import { useI18n } from 'vue-i18n' import ContentToolbar from './ContentToolbar.vue' import AddLink from '@/components/icons/AddLink.vue' -import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui' +import { NButton, NIcon, NInput, useThemeVars } from 'naive-ui' import { types, types as redisTypes } from '@/consts/support_redis_type.js' import EditableTableColumn from '@/components/common/EditableTableColumn.vue' import useDialogStore from 'stores/dialog.js' @@ -14,6 +14,8 @@ import useBrowserStore from 'stores/browser.js' import LoadList from '@/components/icons/LoadList.vue' import LoadAll from '@/components/icons/LoadAll.vue' import IconButton from '@/components/common/IconButton.vue' +import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue' +import Edit from '@/components/icons/Edit.vue' const i18n = useI18n() const themeVars = useThemeVars() @@ -33,7 +35,7 @@ const props = defineProps({ value: Object, size: Number, length: Number, - viewAs: { + format: { type: String, default: formatTypes.PLAIN_TEXT, }, @@ -74,31 +76,27 @@ const currentEditRow = ref({ no: 0, key: '', value: null, + format: formatTypes.PLAIN_TEXT, + decode: decodeTypes.NONE, }) + +const inEdit = computed(() => { + return currentEditRow.value.no > 0 +}) +const tableRef = ref(null) const fieldColumn = reactive({ key: 'key', title: i18n.t('common.field'), align: 'center', titleAlign: 'center', resizable: true, + ellipsis: { + tooltip: true, + }, filterOptionValue: null, filter(value, row) { return !!~row.key.indexOf(value.toString()) }, - // sorter: (row1, row2) => row1.key - row2.key, - render: (row) => { - const isEdit = currentEditRow.value.no === row.no - if (isEdit) { - return h(NInput, { - value: currentEditRow.value.key, - 'onUpdate:value': (val) => { - currentEditRow.value.key = val - }, - }) - } else { - return row.key - } - }, }) const valueColumn = reactive({ key: 'value', @@ -106,34 +104,58 @@ const valueColumn = reactive({ align: 'center', titleAlign: 'center', resizable: true, + ellipsis: { + tooltip: true, + }, filterOptionValue: null, filter(value, row) { return !!~row.value.indexOf(value.toString()) }, - // sorter: (row1, row2) => row1.value - row2.value, - // ellipsis: { - // tooltip: true - // }, - 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 }) - } - }, }) -const cancelEdit = () => { +const startEdit = async ({ no, key, value }) => { + currentEditRow.value.value = value + currentEditRow.value.no = no + currentEditRow.value.key = key +} + +const saveEdit = async (field, value, decode, format) => { + try { + const row = tableData.value[currentEditRow.value.no - 1] + if (row == null) { + throw new Error('row not exists') + } + + const { updated, success, msg } = await browserStore.setHash({ + server: props.name, + db: props.db, + key: keyName.value, + field: row.key, + newField: field, + value, + decode, + format, + }) + if (success) { + row.key = field + row.value = updated[row.key] || '' + $message.success(i18n.t('dialogue.save_value_succ')) + } else { + $message.error(msg) + } + } catch (e) { + $message.error(e.message) + } finally { + resetEdit() + } +} + +const resetEdit = () => { currentEditRow.value.no = 0 + currentEditRow.value.key = '' + currentEditRow.value.value = null + currentEditRow.value.format = formatTypes.PLAIN_TEXT + currentEditRow.value.decode = decodeTypes.NONE } const actionColumn = { @@ -145,13 +167,9 @@ const actionColumn = { fixed: 'right', render: (row) => { return h(EditableTableColumn, { - editing: currentEditRow.value.no === row.no, + editing: false, bindKey: row.key, - onEdit: () => { - currentEditRow.value.no = row.no - currentEditRow.value.key = row.key - currentEditRow.value.value = row.value - }, + onEdit: () => startEdit(row), onDelete: async () => { try { const { success, msg } = await browserStore.removeHashField( @@ -169,43 +187,45 @@ const actionColumn = { $message.error(e.message) } }, - onSave: async () => { - try { - const { success, msg } = await browserStore.setHash( - props.name, - props.db, - keyName.value, - row.key, - currentEditRow.value.key, - 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 = reactive([ - { - key: 'no', - title: '#', - width: 80, - align: 'center', - titleAlign: 'center', - }, - fieldColumn, - valueColumn, - actionColumn, -]) + +const columns = computed(() => { + if (!inEdit.value) { + return [ + { + key: 'no', + title: '#', + width: 80, + align: 'center', + titleAlign: 'center', + }, + fieldColumn, + valueColumn, + actionColumn, + ] + } else { + return [ + { + key: 'no', + title: '#', + width: 80, + align: 'center', + titleAlign: 'center', + render(row) { + if (row.no === currentEditRow.value.no) { + // editing row, show edit state + return h(NIcon, { size: 16, color: 'red' }, () => h(Edit, { strokeWidth: 5 })) + } else { + return row.no + } + }, + }, + fieldColumn, + ] + } +}) const tableData = computed(() => { const data = [] @@ -220,6 +240,17 @@ const tableData = computed(() => { return data }) +const rowProps = (row) => { + return { + onClick: () => { + // in edit mode, switch edit row by click + if (inEdit.value) { + startEdit(row) + } + }, + } +} + const entries = computed(() => { const len = size(tableData.value) return `${len} / ${Math.max(len, props.length)}` @@ -232,12 +263,12 @@ const onAddRow = () => { const filterValue = ref('') const onFilterInput = (val) => { switch (filterType.value) { - case filterOption[0].value: + case filterOption.value[0].value: // filter field valueColumn.filterOptionValue = null fieldColumn.filterOptionValue = val break - case filterOption[1].value: + case filterOption.value[1].value: // filter value fieldColumn.filterOptionValue = null valueColumn.filterOptionValue = val @@ -256,10 +287,10 @@ const clearFilter = () => { const onUpdateFilter = (filters, sourceColumn) => { switch (filterType.value) { - case filterOption[0].value: + case filterOption.value[0].value: fieldColumn.filterOptionValue = filters[sourceColumn.key] break - case filterOption[1].value: + case filterOption.value[1].value: valueColumn.filterOptionValue = filters[sourceColumn.key] break } @@ -268,7 +299,7 @@ const onUpdateFilter = (filters, sourceColumn) => { defineExpose({ reset: () => { clearFilter() - cancelEdit() + resetEdit() }, }) @@ -328,14 +359,17 @@ defineExpose({ {{ $t('interface.add_row') }} -
+
+ + + +
- + {{ $t('interface.memory_usage') }}: {{ bytes(props.size) }}
- - - +
diff --git a/frontend/src/components/content_value/ContentValueWrapper.vue b/frontend/src/components/content_value/ContentValueWrapper.vue index 689b40d..b6766f0 100644 --- a/frontend/src/components/content_value/ContentValueWrapper.vue +++ b/frontend/src/components/content_value/ContentValueWrapper.vue @@ -10,7 +10,6 @@ import { useThemeVars } from 'naive-ui' import useBrowserStore from 'stores/browser.js' import { computed, onMounted, ref, watch } from 'vue' import { isEmpty } from 'lodash' -import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import useDialogStore from 'stores/dialog.js' const themeVars = useThemeVars() @@ -45,6 +44,7 @@ const props = defineProps({ const data = computed(() => { return props.content }) +const initializing = ref(false) const binaryKey = computed(() => { return !!data.value.keyCode @@ -116,22 +116,29 @@ const onLoadAll = () => { loadData(false, true) } -onMounted(() => { +const initContent = async () => { // onReload() - loadData(false, false) -}) - -const contentRef = ref(null) -watch( - () => data.value?.keyPath, - () => { - // onReload() + try { + initializing.value = true if (contentRef.value?.reset != null) { contentRef.value?.reset() } - loadData(false, false) - }, -) + await loadData(false, false) + if (contentRef.value?.beforeShow != null) { + await contentRef.value?.beforeShow() + } + } finally { + initializing.value = false + } +} + +onMounted(() => { + // onReload() + initContent() +}) + +const contentRef = ref(null) +watch(() => data.value?.keyPath, initContent)