diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index a4951a9..07857a8 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -633,23 +633,38 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS //data.Value, data.Decode, data.Format = strutil.ConvertTo(str, param.Decode, param.Format) case "list": - loadListHandle := func() ([]string, bool, error) { - var items []string + loadListHandle := func() ([]types.ListEntryItem, bool, error) { + var loadVal []string var cursor uint64 if param.Full { // load all cursor = 0 - items, err = client.LRange(ctx, key, 0, -1).Result() + loadVal, err = client.LRange(ctx, key, 0, -1).Result() } else { cursor, _ = getEntryCursor() scanSize := int64(Preferences().GetScanSize()) - items, err = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result() + loadVal, err = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result() cursor = cursor + uint64(scanSize) - if len(items) < int(scanSize) { + if len(loadVal) < int(scanSize) { cursor = 0 } } 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 { + items[i].DisplayValue = dv + } + } + } if err != nil { return items, false, err } @@ -657,6 +672,11 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } data.Value, data.End, err = loadListHandle() + data.Decode, data.Format = param.Decode, param.Format + if err != nil { + resp.Msg = err.Error() + return + } case "hash": loadHashHandle := func() ([]types.HashEntryItem, bool, error) { @@ -665,7 +685,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS items := make([]types.HashEntryItem, 0, scanSize) var loadedVal []string var cursor uint64 - var doConvert = len(param.Decode) > 0 && len(param.Format) > 0 + doConvert := len(param.Decode) > 0 && len(param.Format) > 0 if param.Full { // load all cursor = 0 @@ -674,18 +694,16 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS if err != nil { return nil, false, err } - var v string for i := 0; i < len(loadedVal); i += 2 { - if doConvert { - v, _, _ = strutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format) - } else { - v = loadedVal[i+1] - } items = append(items, types.HashEntryItem{ - Key: loadedVal[i], - Value: strutil.EncodeRedisKey(loadedVal[i+1]), - DisplayValue: v, + Key: loadedVal[i], + Value: strutil.EncodeRedisKey(loadedVal[i+1]), }) + if doConvert { + if dv, _, _ := strutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format); dv != loadedVal[i+1] { + items[i/2].DisplayValue = dv + } + } } if cursor == 0 { break @@ -697,18 +715,16 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS if err != nil { return nil, false, err } - var v string for i := 0; i < len(loadedVal); i += 2 { - if doConvert { - v, _, _ = strutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format) - } else { - v = loadedVal[i+1] - } items = append(items, types.HashEntryItem{ - Key: loadedVal[i], - Value: strutil.EncodeRedisKey(loadedVal[i+1]), - DisplayValue: v, + Key: loadedVal[i], + Value: strutil.EncodeRedisKey(loadedVal[i+1]), }) + if doConvert { + if dv, _, _ := strutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format); dv != loadedVal[i+1] { + items[i/2].DisplayValue = dv + } + } } } setEntryCursor(cursor) @@ -751,6 +767,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } data.Value, data.End, err = loadSetHandle() + data.Decode, data.Format = param.Decode, param.Format if err != nil { resp.Msg = err.Error() return @@ -801,6 +818,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } data.Value, data.End, err = loadZSetHandle() + data.Decode, data.Format = param.Decode, param.Format if err != nil { resp.Msg = err.Error() return @@ -849,6 +867,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } data.Value, data.End, err = loadStreamHandle() + data.Decode, data.Format = param.Decode, param.Format if err != nil { resp.Msg = err.Error() return @@ -912,7 +931,7 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp } else { 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()) + resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) return } _, err = client.Set(ctx, key, saveStr, 0).Result() @@ -1023,7 +1042,7 @@ func (b *browserService) SetHashValue(param types.SetHashParam) (resp types.JSRe 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()) + resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error()) return } var removedField []string @@ -1148,20 +1167,21 @@ func (b *browserService) AddListItem(connName string, db int, k any, action int, } // SetListItem update or remove list item by index -func (b *browserService) SetListItem(connName string, db int, k any, index int64, value string) (resp types.JSResp) { - item, err := b.getRedisClient(connName, db) +func (b *browserService) SetListItem(param types.SetListParam) (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 removed []int64 updated := map[int64]string{} - if len(value) <= 0 { + if len(str) <= 0 { // remove from list - err = client.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err() + err = client.LSet(ctx, key, param.Index, "---VALUE_REMOVED_BY_TINY_RDM---").Err() if err != nil { resp.Msg = err.Error() return @@ -1172,15 +1192,20 @@ func (b *browserService) SetListItem(connName string, db int, k any, index int64 resp.Msg = err.Error() return } - removed = append(removed, index) + removed = append(removed, param.Index) } else { // replace index value - err = client.LSet(ctx, key, index, value).Err() + 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.LSet(ctx, key, param.Index, saveStr).Err() if err != nil { resp.Msg = err.Error() return } - updated[index] = value + updated[param.Index] = saveStr } resp.Success = true diff --git a/backend/types/js_resp.go b/backend/types/js_resp.go index de9ab65..7559fbd 100644 --- a/backend/types/js_resp.go +++ b/backend/types/js_resp.go @@ -30,10 +30,15 @@ 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"` + DisplayValue string `json:"dv,omitempty"` } type KeyDetail struct { @@ -55,6 +60,16 @@ type SetKeyParam struct { Decode string `json:"decode,omitempty"` } +type SetListParam struct { + Server string `json:"server"` + DB int `json:"db"` + Key any `json:"key"` + Index int64 `json:"index"` + Value any `json:"value"` + Format string `json:"format,omitempty"` + Decode string `json:"decode,omitempty"` +} + type SetHashParam struct { Server string `json:"server"` DB int `json:"db"` diff --git a/frontend/src/components/content_value/ContentEntryEditor.vue b/frontend/src/components/content_value/ContentEntryEditor.vue index a026505..d4961f8 100644 --- a/frontend/src/components/content_value/ContentEntryEditor.vue +++ b/frontend/src/components/content_value/ContentEntryEditor.vue @@ -9,12 +9,12 @@ import FormatSelector from '@/components/content_value/FormatSelector.vue' const props = defineProps({ field: { - type: String, + type: [String, Number], }, value: { type: String, }, - keyLabel: { + fieldLabel: { type: String, }, valueLabel: { @@ -26,6 +26,9 @@ const props = defineProps({ format: { type: String, }, + fieldReadonly: { + type: Boolean, + }, }) const themeVars = useThemeVars() @@ -58,7 +61,10 @@ const displayValue = computed(() => { if (loading.value) { return '' } - return viewAs.value || decodeRedisKey(props.value) + if (viewAs.value == null) { + return decodeRedisKey(props.value) + } + return viewAs.value }) /** @@ -79,7 +85,7 @@ const onFormatChanged = async (decode = '', format = '') => { decode, format, }) - viewAs.field = props.field + viewAs.field = props.field + '' viewAs.value = value viewAs.decode = decode || retDecode viewAs.format = format || retFormat @@ -110,8 +116,12 @@ const onSave = () => {
-
{{ props.keyLabel }}
- +
{{ props.fieldLabel }}
+
diff --git a/frontend/src/components/content_value/ContentValueHash.vue b/frontend/src/components/content_value/ContentValueHash.vue index 2b711d3..320924c 100644 --- a/frontend/src/components/content_value/ContentValueHash.vue +++ b/frontend/src/components/content_value/ContentValueHash.vue @@ -99,10 +99,10 @@ const fieldColumn = reactive({ tooltip: true, }, filterOptionValue: null, - filter(value, row) { + filter: (value, row) => { return !!~row.k.indexOf(value.toString()) }, - render(row) { + render: (row) => { return decodeRedisKey(row.k) }, }) @@ -116,15 +116,15 @@ const valueColumn = reactive({ tooltip: true, }, filterOptionValue: null, - filter(value, row) { - return !!~row.value.indexOf(value.toString()) + filter: (value, row) => { + return !!~row.v.indexOf(value.toString()) }, - render(row) { - return row.dv + render: (row) => { + return row.dv || row.v }, }) -const startEdit = async ({ no, key, value }) => { +const startEdit = async (no, key, value) => { currentEditRow.value = value currentEditRow.no = no currentEditRow.key = key @@ -186,7 +186,7 @@ const actionColumn = { return h(EditableTableColumn, { editing: false, bindKey: row.k, - onEdit: () => startEdit({ no: index + 1, key: row.k, value: row.v }), + onEdit: () => startEdit(index + 1, row.k, row.v), onDelete: async () => { try { const { success, msg } = await browserStore.removeHashField( @@ -217,7 +217,7 @@ const columns = computed(() => { width: 80, align: 'center', titleAlign: 'center', - render(row, index) { + render: (row, index) => { return index + 1 }, }, @@ -233,7 +233,7 @@ const columns = computed(() => { width: 80, align: 'center', titleAlign: 'center', - render(row, index) { + render: (row, index) => { if (index + 1 === currentEditRow.no) { // editing row, show edit state return h(NIcon, { size: 16, color: 'red' }, () => h(Edit, { strokeWidth: 5 })) @@ -252,7 +252,7 @@ const rowProps = (row, index) => { onClick: () => { // in edit mode, switch edit row by click if (inEdit.value) { - startEdit({ no: index + 1, key: row.k, value: row.v }) + startEdit(index + 1, row.k, row.v) } }, } @@ -270,12 +270,12 @@ const onAddRow = () => { const filterValue = ref('') const onFilterInput = (val) => { switch (filterType.value) { - case filterOption.value[0].value: + case filterOption[0].value: // filter field valueColumn.filterOptionValue = null fieldColumn.filterOptionValue = val break - case filterOption.value[1].value: + case filterOption[1].value: // filter value fieldColumn.filterOptionValue = null valueColumn.filterOptionValue = val @@ -294,10 +294,10 @@ const clearFilter = () => { const onUpdateFilter = (filters, sourceColumn) => { switch (filterType.value) { - case filterOption.value[0].value: + case filterOption[0].value: fieldColumn.filterOptionValue = filters[sourceColumn.key] break - case filterOption.value[1].value: + case filterOption[1].value: valueColumn.filterOptionValue = filters[sourceColumn.key] break } @@ -395,8 +395,8 @@ defineExpose({ v-show="inEdit" :decode="currentEditRow.decode" :field="currentEditRow.key" + :field-label="$t('common.field')" :format="currentEditRow.format" - :key-label="$t('common.field')" :value="currentEditRow.value" :value-label="$t('common.value')" class="flex-item-expand" diff --git a/frontend/src/components/content_value/ContentValueList.vue b/frontend/src/components/content_value/ContentValueList.vue index 4b886ff..5fdb86e 100644 --- a/frontend/src/components/content_value/ContentValueList.vue +++ b/frontend/src/components/content_value/ContentValueList.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 { isEmpty, size } from 'lodash' import { types, types as redisTypes } from '@/consts/support_redis_type.js' import EditableTableColumn from '@/components/common/EditableTableColumn.vue' @@ -14,6 +14,9 @@ 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 FormatSelector from '@/components/content_value/FormatSelector.vue' +import Edit from '@/components/icons/Edit.vue' const i18n = useI18n() const themeVars = useThemeVars() @@ -33,7 +36,7 @@ const props = defineProps({ value: Object, size: Number, length: Number, - viewAs: { + format: { type: String, default: formatTypes.RAW, }, @@ -58,39 +61,80 @@ const keyName = computed(() => { const browserStore = useBrowserStore() const dialogStore = useDialogStore() const keyType = redisTypes.LIST -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)) { + // console.log(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 +} + +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 { updated, success, msg } = await browserStore.updateListItem({ + server: props.name, + db: props.db, + key: keyName.value, + index, + value, + decode, + format, + }) + if (success) { + row.v = updated[index] || '' + 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 = { @@ -100,13 +144,14 @@ const actionColumn = { align: 'center', titleAlign: 'center', fixed: 'right', - render: (row) => { + render: (row, index) => { return h(EditableTableColumn, { - editing: currentEditRow.value.no === row.no, - bindKey: '#' + row.no, + editing: false, + bindKey: `#${index + 1}`, onEdit: () => { - currentEditRow.value.no = row.no - currentEditRow.value.value = row.value + currentEditRow.no = index + 1 + currentEditRow.value = row + startEdit(index + 1, row.v) }, onDelete: async () => { try { @@ -114,7 +159,7 @@ const actionColumn = { props.name, props.db, keyName.value, - row.no - 1, + index, ) if (success) { $message.success(i18n.t('dialogue.delete_key_succ', { key: '#' + row.no })) @@ -125,58 +170,61 @@ const actionColumn = { $message.error(e.message) } }, - onSave: async () => { - try { - const { success, msg } = await browserStore.updateListItem( - props.name, - props.db, - keyName.value, - currentEditRow.value.no - 1, - 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)}` }) @@ -197,10 +245,14 @@ const onUpdateFilter = (filters, sourceColumn) => { valueColumn.filterOptionValue = filters[sourceColumn.key] } +const onFormatChanged = (selDecode, selFormat) => { + emit('reload', selDecode, selFormat) +} + defineExpose({ reset: () => { clearFilter() - cancelEdit() + resetEdit() }, }) @@ -252,14 +304,16 @@ defineExpose({ {{ $t('interface.add_row') }}
-
+
+ + + +
diff --git a/frontend/src/langs/en-us.json b/frontend/src/langs/en-us.json index 425159d..45e8f3d 100644 --- a/frontend/src/langs/en-us.json +++ b/frontend/src/langs/en-us.json @@ -16,7 +16,8 @@ "all": "All", "key": "Key", "value": "Value", - "field": "Field" + "field": "Field", + "index": "Position" }, "preferences": { "name": "Preferences", diff --git a/frontend/src/langs/pt-br.json b/frontend/src/langs/pt-br.json index f5fcced..3436b24 100644 --- a/frontend/src/langs/pt-br.json +++ b/frontend/src/langs/pt-br.json @@ -16,7 +16,8 @@ "all": "Tudo", "key": "Chave", "value": "Valor", - "field": "Campo" + "field": "Campo", + "index": "Posição" }, "preferences": { "name": "Preferências", diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index 6efc4b1..86cb6de 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -16,7 +16,8 @@ "all": "全部", "key": "键", "value": "值", - "field": "字段" + "field": "字段", + "index": "位置" }, "preferences": { "name": "偏好设置", diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js index 5298c3b..371c2e9 100644 --- a/frontend/src/stores/browser.js +++ b/frontend/src/stores/browser.js @@ -1191,28 +1191,30 @@ const useBrowserStore = defineStore('browser', { /** * update value of list item by index - * @param {string} connName + * @param {string} server * @param {number} db * @param {string|number[]} key * @param {number} index - * @param {string} value + * @param {string|number[]} value + * @param {string} decode + * @param {string} format * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} */ - async updateListItem(connName, db, key, index, value) { + async updateListItem({ server, db, key, index, value, decode, format }) { try { - const { data, success, msg } = await SetListItem(connName, db, key, index, value) + const { data, success, msg } = await SetListItem({ server, db, key, index, value, decode, format }) if (success) { const { updated = {} } = data - if (!isEmpty(updated)) { - const tab = useTabStore() - tab.upsertValueEntries({ - server: connName, - db, - key, - type: 'list', - entries: updated, - }) - } + // if (!isEmpty(updated)) { + // const tab = useTabStore() + // tab.upsertValueEntries({ + // server, + // db, + // key, + // type: 'list', + // entries: updated, + // }) + // } return { success, updated } } else { return { success, msg } @@ -1224,21 +1226,21 @@ const useBrowserStore = defineStore('browser', { /** * remove list item - * @param {string} connName + * @param {string} server * @param {number} db * @param {string|number[]} key * @param {number} index * @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>} */ - async removeListItem(connName, db, key, index) { + async removeListItem(server, db, key, index) { try { - const { data, success, msg } = await SetListItem(connName, db, key, index, '') + const { data, success, msg } = await SetListItem({ server, db, key, index }) if (success) { const { removed = [] } = data if (!isEmpty(removed)) { const tab = useTabStore() tab.removeValueEntries({ - server: connName, + server, db, key, type: 'list',