diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index caeb4fe..428d37d 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -595,7 +595,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } // define get entry cursor function - getEntryCursor := func() (uint64, string) { + getEntryCursor := func() (uint64, string, bool) { if entry, ok := entryCors[param.DB]; !ok || entry.Key != key || entry.Pattern != matchPattern { // not the same key or match pattern, reset cursor entry = entryCursor{ @@ -605,9 +605,9 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS Cursor: 0, } entryCors[param.DB] = entry - return 0, "" + return 0, "", true } else { - return entry.Cursor, entry.XLast + return entry.Cursor, entry.XLast, false } } // define set entry cursor function @@ -639,19 +639,21 @@ 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() ([]types.ListEntryItem, bool, error) { + loadListHandle := func() ([]types.ListEntryItem, bool, bool, error) { var loadVal []string var cursor uint64 + var reset bool var subErr error - if param.Full { + doFilter := matchPattern != "*" + if param.Full || matchPattern != "*" { // load all - cursor = 0 + cursor, reset = 0, true loadVal, subErr = client.LRange(ctx, key, 0, -1).Result() } else { if param.Reset { - cursor = 0 + cursor, reset = 0, true } else { - cursor, _ = getEntryCursor() + cursor, _, reset = getEntryCursor() } scanSize := int64(Preferences().GetScanSize()) loadVal, subErr = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result() @@ -662,42 +664,48 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } setEntryCursor(cursor) - items := make([]types.ListEntryItem, len(loadVal)) - for i, val := range loadVal { - items[i].Value = val + items := make([]types.ListEntryItem, 0, len(loadVal)) + for _, val := range loadVal { + if doFilter && !strings.Contains(val, param.MatchPattern) { + continue + } + items = append(items, types.ListEntryItem{ + Value: val, + }) if doConvert { if dv, _, _ := strutil.ConvertTo(val, param.Decode, param.Format); dv != val { - items[i].DisplayValue = dv + items[len(items)-1].DisplayValue = dv } } } if subErr != nil { - return items, false, subErr + return items, reset, false, subErr } - return items, cursor == 0, nil + return items, reset, cursor == 0, nil } - data.Value, data.End, err = loadListHandle() - data.Decode, data.Format = param.Decode, param.Format + data.Value, data.Reset, data.End, err = loadListHandle() + data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format if err != nil { resp.Msg = err.Error() return } case "hash": - loadHashHandle := func() ([]types.HashEntryItem, bool, error) { + loadHashHandle := func() ([]types.HashEntryItem, bool, bool, error) { var items []types.HashEntryItem var loadedVal []string var cursor uint64 + var reset bool var subErr error scanSize := int64(Preferences().GetScanSize()) if param.Full { // load all - cursor = 0 + cursor, reset = 0, true for { loadedVal, cursor, subErr = client.HScan(ctx, key, cursor, "*", scanSize).Result() if subErr != nil { - return nil, false, subErr + return nil, reset, false, subErr } for i := 0; i < len(loadedVal); i += 2 { items = append(items, types.HashEntryItem{ @@ -716,13 +724,13 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } else { if param.Reset { - cursor = 0 + cursor, reset = 0, true } else { - cursor, _ = getEntryCursor() + cursor, _, reset = getEntryCursor() } loadedVal, cursor, subErr = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result() if subErr != nil { - return nil, false, subErr + return nil, reset, false, subErr } loadedLen := len(loadedVal) items = make([]types.HashEntryItem, loadedLen/2) @@ -737,30 +745,31 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } setEntryCursor(cursor) - return items, cursor == 0, nil + return items, reset, cursor == 0, nil } - data.Value, data.End, err = loadHashHandle() - data.Decode, data.Format = param.Decode, param.Format + data.Value, data.Reset, data.End, err = loadHashHandle() + data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format if err != nil { resp.Msg = err.Error() return } case "set": - loadSetHandle := func() ([]types.SetEntryItem, bool, error) { + loadSetHandle := func() ([]types.SetEntryItem, bool, bool, error) { var items []types.SetEntryItem var cursor uint64 + var reset bool var subErr error var loadedKey []string scanSize := int64(Preferences().GetScanSize()) if param.Full { // load all - cursor = 0 + cursor, reset = 0, true for { loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() if subErr != nil { - return items, false, subErr + return items, reset, false, subErr } for _, val := range loadedKey { items = append(items, types.SetEntryItem{ @@ -778,9 +787,9 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } else { if param.Reset { - cursor = 0 + cursor, reset = 0, true } else { - cursor, _ = getEntryCursor() + cursor, _, reset = getEntryCursor() } loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() items = make([]types.SetEntryItem, len(loadedKey)) @@ -794,29 +803,30 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } setEntryCursor(cursor) - return items, cursor == 0, nil + return items, reset, cursor == 0, nil } - data.Value, data.End, err = loadSetHandle() - data.Decode, data.Format = param.Decode, param.Format + data.Value, data.Reset, data.End, err = loadSetHandle() + data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format if err != nil { resp.Msg = err.Error() return } case "zset": - loadZSetHandle := func() ([]types.ZSetEntryItem, bool, error) { + loadZSetHandle := func() ([]types.ZSetEntryItem, bool, bool, error) { var items []types.ZSetEntryItem + var reset bool var cursor uint64 scanSize := int64(Preferences().GetScanSize()) var loadedVal []string if param.Full { // load all - cursor = 0 + cursor, reset = 0, true for { loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() if err != nil { - return items, false, err + return items, reset, false, err } var score float64 for i := 0; i < len(loadedVal); i += 2 { @@ -838,9 +848,9 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } else { if param.Reset { - cursor = 0 + cursor, reset = 0, true } else { - cursor, _ = getEntryCursor() + cursor, _, reset = getEntryCursor() } loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() loadedLen := len(loadedVal) @@ -859,30 +869,31 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } setEntryCursor(cursor) - return items, cursor == 0, nil + return items, reset, cursor == 0, nil } - data.Value, data.End, err = loadZSetHandle() - data.Decode, data.Format = param.Decode, param.Format + data.Value, data.Reset, data.End, err = loadZSetHandle() + data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format if err != nil { resp.Msg = err.Error() return } case "stream": - loadStreamHandle := func() ([]types.StreamEntryItem, bool, error) { + loadStreamHandle := func() ([]types.StreamEntryItem, bool, bool, error) { var msgs []redis.XMessage var last string + var reset bool if param.Full { // load all - last = "" + last, reset = "", true msgs, err = client.XRevRange(ctx, key, "+", "-").Result() } else { scanSize := int64(Preferences().GetScanSize()) if param.Reset { last = "" } else { - _, last = getEntryCursor() + _, last, reset = getEntryCursor() } if len(last) <= 0 { last = "+" @@ -913,13 +924,13 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS } } if err != nil { - return items, false, err + return items, reset, false, err } - return items, last == "", nil + return items, reset, last == "", nil } - data.Value, data.End, err = loadStreamHandle() - data.Decode, data.Format = param.Decode, param.Format + data.Value, data.Reset, data.End, err = loadStreamHandle() + data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format if err != nil { resp.Msg = err.Error() return diff --git a/backend/types/js_resp.go b/backend/types/js_resp.go index 6e5aabf..ceb4e8a 100644 --- a/backend/types/js_resp.go +++ b/backend/types/js_resp.go @@ -35,6 +35,8 @@ type KeyDetail struct { Length int64 `json:"length,omitempty"` Format string `json:"format,omitempty"` Decode string `json:"decode,omitempty"` + Match string `json:"match,omitempty"` + Reset bool `json:"reset"` End bool `json:"end"` } diff --git a/frontend/src/components/content/ContentPane.vue b/frontend/src/components/content/ContentPane.vue index 4ce294c..3fff5e3 100644 --- a/frontend/src/components/content/ContentPane.vue +++ b/frontend/src/components/content/ContentPane.vue @@ -57,6 +57,7 @@ const tabContent = computed(() => { length: tab.length || 0, decode: tab.decode || decodeTypes.NONE, format: tab.format || formatTypes.RAW, + matchPattern: tab.matchPattern || '', end: tab.end === true, loading: tab.loading === true, } diff --git a/frontend/src/components/content_value/ContentSearchInput.vue b/frontend/src/components/content_value/ContentSearchInput.vue new file mode 100644 index 0000000..826d42b --- /dev/null +++ b/frontend/src/components/content_value/ContentSearchInput.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/frontend/src/components/content_value/ContentValueList.vue b/frontend/src/components/content_value/ContentValueList.vue index e3c23b5..88bc2f3 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, NCode, NIcon, 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' @@ -17,6 +17,7 @@ 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' +import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue' const i18n = useI18n() const themeVars = useThemeVars() @@ -52,7 +53,7 @@ const props = defineProps({ loading: Boolean, }) -const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete']) +const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete', 'match']) /** * @@ -249,13 +250,13 @@ const onAddValue = (value) => { dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.LIST) } -const filterValue = ref('') const onFilterInput = (val) => { valueFilterOption.value = val } -const clearFilter = () => { - valueFilterOption.value = null +const onMatchInput = (matchVal, filterVal) => { + valueFilterOption.value = filterVal + emit('match', matchVal) } const onUpdateFilter = (filters, sourceColumn) => { @@ -266,10 +267,11 @@ const onFormatChanged = (selDecode, selFormat) => { emit('reload', selDecode, selFormat) } +const searchInputRef = ref(null) defineExpose({ reset: () => { - clearFilter() resetEdit() + searchInputRef.value?.reset() }, }) @@ -290,12 +292,10 @@ defineExpose({ @rename="emit('rename')" />
- +
diff --git a/frontend/src/components/content_value/ContentValueWrapper.vue b/frontend/src/components/content_value/ContentValueWrapper.vue index cef5aa0..4e7f15c 100644 --- a/frontend/src/components/content_value/ContentValueWrapper.vue +++ b/frontend/src/components/content_value/ContentValueWrapper.vue @@ -64,7 +64,14 @@ const keyName = computed(() => { return !isEmpty(data.value.keyCode) ? data.value.keyCode : data.value.keyPath }) -const loadData = async (reset, full) => { +/** + * + * @param {boolean} reset + * @param {boolean} [full] + * @param {string} [selMatch] + * @return {Promise} + */ +const loadData = async (reset, full, selMatch) => { try { if (!!props.blank) { return @@ -75,7 +82,7 @@ const loadData = async (reset, full) => { server: name, db: db, key: keyName.value, - matchPattern: matchPattern, + matchPattern: selMatch === undefined ? matchPattern : selMatch, decode: reset ? decodeTypes.NONE : decode, format: reset ? formatTypes.RAW : format, reset, @@ -133,6 +140,10 @@ const onLoadAll = () => { loadData(false, true) } +const onMatch = (match) => { + loadData(true, false, match || '') +} + const contentRef = ref(null) const initContent = async () => { // onReload() @@ -141,7 +152,7 @@ const initContent = async () => { if (contentRef.value?.reset != null) { contentRef.value?.reset() } - await loadData(true, false) + await loadData(true, false, '') if (contentRef.value?.beforeShow != null) { await contentRef.value?.beforeShow() } @@ -185,6 +196,7 @@ watch(() => data.value?.keyPath, initContent) @delete="onDelete" @loadall="onLoadAll" @loadmore="onLoadMore" + @match="onMatch" @reload="onReload" @rename="onRename" /> diff --git a/frontend/src/langs/en-us.json b/frontend/src/langs/en-us.json index c619585..3962571 100644 --- a/frontend/src/langs/en-us.json +++ b/frontend/src/langs/en-us.json @@ -81,6 +81,7 @@ "pin_edit": "Pin Editor(Do not close after save)", "unpin_edit": "Cancel Pin", "search": "Search", + "full_search": "Full Search", "filter_field": "Filter Field", "filter_value": "Filter Value", "length": "Length", diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index 2fc6891..c19652b 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -81,6 +81,7 @@ "pin_edit": "固定编辑框(保存后不关闭)", "unpin_edit": "取消固定", "search": "搜索", + "full_search": "全文搜索", "filter_field": "筛选字段", "filter_value": "筛选值", "length": "长度", diff --git a/frontend/src/stores/browser.js b/frontend/src/stores/browser.js index 860464d..4a806f4 100644 --- a/frontend/src/stores/browser.js +++ b/frontend/src/stores/browser.js @@ -429,14 +429,15 @@ const useBrowserStore = defineStore('browser', { * @param {string|number[]} key * @param {string} [decode] * @param {string} [format] + * @param {string} [matchPattern] * @return {Promise} */ - async reloadKey({ server, db, key, decode, format }) { + async reloadKey({ server, db, key, decode, format, matchPattern }) { const tab = useTabStore() try { tab.updateLoading({ server, db, loading: true }) await this.loadKeySummary({ server, db, key }) - await this.loadKeyDetail({ server, db, key, decode, format, reset: true }) + await this.loadKeyDetail({ server, db, key, decode, format, matchPattern, reset: true }) } finally { tab.updateLoading({ server, db, loading: false }) } @@ -470,7 +471,7 @@ const useBrowserStore = defineStore('browser', { lite: true, }) if (success) { - const { value, decode: retDecode, format: retFormat, end } = data + const { value, decode: retDecode, format: retFormat, match: retMatch, reset: retReset, end } = data tab.updateValue({ server, db, @@ -478,7 +479,8 @@ const useBrowserStore = defineStore('browser', { value, decode: retDecode, format: retFormat, - reset: reset || full === true, + reset: retReset, + matchPattern: retMatch || '', end, }) } else { diff --git a/frontend/src/stores/tab.js b/frontend/src/stores/tab.js index 4ced3e3..d7ecab0 100644 --- a/frontend/src/stores/tab.js +++ b/frontend/src/stores/tab.js @@ -21,6 +21,7 @@ const useTabStore = defineStore('tab', { * @property {int} [ttl] ttl of current key * @param {string} [decode] * @param {string} [format] + * @param {string} [matchPattern] * @param {boolean} [end] * @param {boolean} [loading] */ @@ -159,9 +160,10 @@ const useTabStore = defineStore('tab', { * @param {string} [keyCode] * @param {number} [size] * @param {number} [length] + * @param {string} [matchPattern] * @param {*} [value] */ - upsertTab({ subTab, server, db, type, ttl, key, keyCode, size, length }) { + upsertTab({ subTab, server, db, type, ttl, key, keyCode, size, length, matchPattern = '' }) { let tabIndex = findIndex(this.tabList, { name: server }) if (tabIndex === -1) { this.tabList.push({ @@ -176,6 +178,7 @@ const useTabStore = defineStore('tab', { keyCode, size, length, + matchPattern, value: undefined, }) tabIndex = this.tabList.length - 1 @@ -193,6 +196,7 @@ const useTabStore = defineStore('tab', { tab.keyCode = keyCode tab.size = size tab.length = length + tab.matchPattern = matchPattern tab.value = undefined } this._setActivatedIndex(tabIndex, true, subTab) @@ -207,29 +211,31 @@ const useTabStore = defineStore('tab', { * @param {*} value * @param {string} [format] * @param {string] [decode] + * @param {string} [matchPattern] * @param {boolean} reset * @param {boolean} [end] keep end status if not set */ - updateValue({ server, db, key, value, format, decode, reset, end }) { - const tab = find(this.tabList, { name: server, db, key }) - if (tab == null) { + updateValue({ server, db, key, value, format, decode, matchPattern, reset, end }) { + const tabData = find(this.tabList, { name: server, db, key }) + if (tabData == null) { return } - tab.format = format || tab.format - tab.decode = decode || tab.decode + tabData.format = format || tabData.format + tabData.decode = decode || tabData.decode + tabData.matchPattern = matchPattern || '' if (typeof end === 'boolean') { - tab.end = end + tabData.end = end } if (!reset && typeof value === 'object') { if (value instanceof Array) { - tab.value = tab.value || [] - tab.value.push(...value) + tabData.value = tabData.value || [] + tabData.value.push(...value) } else { - tab.value = assign(value, tab.value || {}) + tabData.value = assign(value, tabData.value || {}) } } else { - tab.value = value + tabData.value = value } },