feat: add key filter for database scan

This commit is contained in:
tiny-craft 2023-07-17 17:17:52 +08:00
parent ea2c24a5f9
commit 41e5ecfad2
8 changed files with 235 additions and 13 deletions

View File

@ -179,7 +179,7 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
return return
} }
// get total database // get total databases
config, err := rdb.ConfigGet(ctx, "databases").Result() config, err := rdb.ConfigGet(ctx, "databases").Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -347,12 +347,12 @@ func (c *connectionService) ServerInfo(name string) (resp types.JSResp) {
// OpenDatabase open select database, and list all keys // OpenDatabase open select database, and list all keys
// @param path contain connection name and db name // @param path contain connection name and db name
func (c *connectionService) OpenDatabase(connName string, db int) (resp types.JSResp) { func (c *connectionService) OpenDatabase(connName string, db int, match string) (resp types.JSResp) {
return c.ScanKeys(connName, db, "*") return c.ScanKeys(connName, db, match)
} }
// ScanKeys scan all keys below prefix // ScanKeys scan all keys
func (c *connectionService) ScanKeys(connName string, db int, prefix string) (resp types.JSResp) { func (c *connectionService) ScanKeys(connName string, db int, match string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db) rdb, ctx, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -364,7 +364,7 @@ func (c *connectionService) ScanKeys(connName string, db int, prefix string) (re
var cursor uint64 var cursor uint64
for { for {
var loadedKey []string var loadedKey []string
loadedKey, cursor, err = rdb.Scan(ctx, cursor, prefix, 10000).Result() loadedKey, cursor, err = rdb.Scan(ctx, cursor, match, 10000).Result()
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return

View File

@ -17,6 +17,7 @@ import usePreferencesStore from './stores/preferences.js'
import useConnectionStore from './stores/connections.js' import useConnectionStore from './stores/connections.js'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { darkTheme, lightTheme, useOsTheme } from 'naive-ui' import { darkTheme, lightTheme, useOsTheme } from 'naive-ui'
import KeyFilterDialog from './components/dialogs/KeyFilterDialog.vue'
hljs.registerLanguage('json', json) hljs.registerLanguage('json', json)
hljs.registerLanguage('plaintext', plaintext) hljs.registerLanguage('plaintext', plaintext)
@ -95,6 +96,7 @@ const theme = computed(() => {
<connection-dialog /> <connection-dialog />
<group-dialog /> <group-dialog />
<new-key-dialog /> <new-key-dialog />
<key-filter-dialog />
<add-fields-dialog /> <add-fields-dialog />
<rename-key-dialog /> <rename-key-dialog />
<delete-key-dialog /> <delete-key-dialog />

View File

@ -0,0 +1,91 @@
<script setup>
import { reactive, ref, watch } from 'vue'
import useDialog from '../../stores/dialog'
import { useI18n } from 'vue-i18n'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()
const filterForm = reactive({
server: '',
db: 0,
pattern: '',
})
const filterFormRef = ref(null)
const formLabelWidth = '100px'
const dialogStore = useDialog()
watch(
() => dialogStore.keyFilterDialogVisible,
(visible) => {
if (visible) {
const { server, db, pattern } = dialogStore.keyFilterParam
filterForm.server = server
filterForm.db = db || 0
filterForm.pattern = pattern || '*'
}
}
)
const connectionStore = useConnectionStore()
const onConfirm = () => {
const { server, db, pattern } = filterForm
connectionStore.setKeyFilter(server, db, pattern)
connectionStore.reopenDatabase(server, db)
}
const onClose = () => {
dialogStore.closeKeyFilterDialog()
}
</script>
<template>
<n-modal
v-model:show="dialogStore.keyFilterDialogVisible"
:closable="false"
:close-on-esc="false"
:mask-closable="false"
:negative-button-props="{ size: 'medium' }"
:negative-text="$t('cancel')"
:positive-button-props="{ size: 'medium' }"
:positive-text="$t('confirm')"
:show-icon="false"
:title="$t('set_key_filter')"
preset="dialog"
style="width: 450px"
transform-origin="center"
@positive-click="onConfirm"
@negative-click="onClose"
>
<n-form
ref="filterFormRef"
:label-width="formLabelWidth"
:model="filterForm"
:show-require-mark="false"
label-align="right"
label-placement="left"
style="padding-right: 15px"
>
<n-form-item :label="$t('server')" path="key">
<n-text>{{ filterForm.server }}</n-text>
</n-form-item>
<n-form-item :label="$t('db_index')" path="db">
<n-text>{{ filterForm.db }}</n-text>
</n-form-item>
<n-form-item :label="$t('filter_pattern')" required>
<n-input-group>
<n-tooltip>
<template #trigger>
<n-input v-model:value="filterForm.pattern" placeholder="Filter Pattern" clearable />
</template>
<div class="text-block">{{ $t('filter_pattern_tip') }}</div>
</n-tooltip>
<n-button secondary type="primary" @click="filterForm.pattern = '*'">
{{ $t('restore_defaults') }}
</n-button>
</n-input-group>
</n-form-item>
</n-form>
</n-modal>
</template>
<style lang="scss" scoped></style>

View File

@ -1,10 +1,10 @@
<script setup> <script setup>
import { computed, h, nextTick, onMounted, reactive, ref } from 'vue' import { computed, h, nextTick, onMounted, reactive, ref } from 'vue'
import { ConnectionType } from '../../consts/connection_type.js' import { ConnectionType } from '../../consts/connection_type.js'
import { NIcon, useDialog, useMessage } from 'naive-ui' import { NIcon, NTag, useDialog, useMessage } from 'naive-ui'
import Key from '../icons/Key.vue' import Key from '../icons/Key.vue'
import ToggleDb from '../icons/ToggleDb.vue' import ToggleDb from '../icons/ToggleDb.vue'
import { get, indexOf, isEmpty } from 'lodash' import { get, indexOf, isEmpty, remove } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Refresh from '../icons/Refresh.vue' import Refresh from '../icons/Refresh.vue'
import CopyLink from '../icons/CopyLink.vue' import CopyLink from '../icons/CopyLink.vue'
@ -18,6 +18,8 @@ import useConnectionStore from '../../stores/connections.js'
import { useConfirmDialog } from '../../utils/confirm_dialog.js' import { useConfirmDialog } from '../../utils/confirm_dialog.js'
import ToggleServer from '../icons/ToggleServer.vue' import ToggleServer from '../icons/ToggleServer.vue'
import Unlink from '../icons/Unlink.vue' import Unlink from '../icons/Unlink.vue'
import Filter from '../icons/Filter.vue'
import Close from '../icons/Close.vue'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -86,6 +88,20 @@ const menuOptions = {
label: i18n.t('new_key'), label: i18n.t('new_key'),
icon: renderIcon(Add), icon: renderIcon(Add),
}, },
{
key: 'db_filter',
label: i18n.t('filter_key'),
icon: renderIcon(Filter),
},
{
type: 'divider',
key: 'd1',
},
{
key: 'db_close',
label: i18n.t('close_db'),
icon: renderIcon(Close),
},
] ]
} else { } else {
return [ return [
@ -261,6 +277,26 @@ const renderSuffix = ({ option }) => {
// return h(NButton, // return h(NButton,
// { text: true, type: 'primary' }, // { text: true, type: 'primary' },
// { default: () => h(Key) }) // { default: () => h(Key) })
if (option.type === ConnectionType.RedisDB) {
const { name: server, db } = option
const filterPattern = connectionStore.getKeyFilter(server, db)
if (!isEmpty(filterPattern) && filterPattern !== '*') {
return h(
NTag,
{
bordered: false,
closable: true,
size: 'small',
onClose: () => {
connectionStore.removeKeyFilter(server, db)
connectionStore.reopenDatabase(server, db)
},
},
{ default: () => filterPattern }
)
}
}
return null
} }
const nodeProps = ({ option }) => { const nodeProps = ({ option }) => {
@ -334,10 +370,18 @@ const handleSelectContextMenu = (key) => {
case 'db_reload': case 'db_reload':
connectionStore.reopenDatabase(props.server, db) connectionStore.reopenDatabase(props.server, db)
break break
case 'db_close':
remove(expandedKeys.value, (k) => k === `${props.server}/db${db}`)
connectionStore.closeDatabase(props.server, db)
break
case 'db_newkey': case 'db_newkey':
case 'key_newkey': case 'key_newkey':
dialogStore.openNewKeyDialog(redisKey, props.server, db) dialogStore.openNewKeyDialog(redisKey, props.server, db)
break break
case 'db_filter':
const pattern = connectionStore.getKeyFilter(props.server, db)
dialogStore.openKeyFilterDialog(props.server, db, pattern)
break
case 'key_reload': case 'key_reload':
connectionStore.loadKeys(props.server, db, redisKey) connectionStore.loadKeys(props.server, db, redisKey)
break break

View File

@ -39,7 +39,8 @@
"view_as": "View As", "view_as": "View As",
"reload": "Reload", "reload": "Reload",
"open_connection": "Open Connection", "open_connection": "Open Connection",
"open_db": "Expand Database", "open_db": "Open Database",
"close_db": "Close Database",
"filter_key": "Filter Keys", "filter_key": "Filter Keys",
"disconnect": "Disconnect", "disconnect": "Disconnect",
"dup_conn": "Duplicate Connection", "dup_conn": "Duplicate Connection",
@ -94,6 +95,10 @@
"enter_elem": "Enter Element", "enter_elem": "Enter Element",
"enter_member": "Enter Member", "enter_member": "Enter Member",
"enter_score": "Enter Score", "enter_score": "Enter Score",
"element": "Element",
"set_key_filter": "Set Key Filter",
"filter_pattern": "Pattern",
"filter_pattern_tip": "prefix_*: Matches key names starting with \"prefix_\".\n*_suffix: Matches key names ending with \"_suffix\".\n*pattern*: Matches key names containing \"pattern\".\nprefix_??: Matches key names starting with \"prefix_\" followed by any two characters.\n*abc*: Matches key names containing \"abc\" at any position.",
"key": "Key", "key": "Key",
"value": "Value", "value": "Value",
"field": "Field", "field": "Field",

View File

@ -41,7 +41,8 @@
"view_as": "查看方式", "view_as": "查看方式",
"reload": "重新载入", "reload": "重新载入",
"open_connection": "打开连接", "open_connection": "打开连接",
"open_db": "展开数据库", "open_db": "打开数据库",
"close_db": "关闭数据库",
"filter_key": "过滤键", "filter_key": "过滤键",
"disconnect": "断开连接", "disconnect": "断开连接",
"dup_conn": "复制连接", "dup_conn": "复制连接",
@ -97,6 +98,9 @@
"enter_member": "输入成员", "enter_member": "输入成员",
"enter_score": "输入分值", "enter_score": "输入分值",
"element": "元素", "element": "元素",
"set_key_filter": "设置键过滤器",
"filter_pattern": "过滤表达式",
"filter_pattern_tip": "prefix_*:匹配以\"prefix_\"开头的键名\n*_suffix匹配以\"_suffix\"结尾的键名\n*pattern*:匹配包含\"pattern\"的键名\nprefix_??:匹配以\"prefix_\"开头后跟两个任意字符的键名\n*abc*:匹配包含\"abc\"的任意位置的键名",
"key": "键", "key": "键",
"value": "值", "value": "值",
"field": "字段", "field": "字段",

View File

@ -59,7 +59,9 @@ const useConnectionStore = defineStore('connections', {
* @typedef {Object} ConnectionState * @typedef {Object} ConnectionState
* @property {string[]} groups * @property {string[]} groups
* @property {ConnectionItem[]} connections * @property {ConnectionItem[]} connections
* @property {Object} serverStats
* @property {Object.<string, ConnectionProfile>} serverProfile * @property {Object.<string, ConnectionProfile>} serverProfile
* @property {Object.<string, string>} keyFilter key is 'server#db', 'server#-1' stores default filter pattern
* @property {Object.<string, DatabaseItem[]>} databases * @property {Object.<string, DatabaseItem[]>} databases
* @property {Object.<string, Map<string, DatabaseItem>>} nodeMap key format likes 'server#db', children key format likes 'key#type' * @property {Object.<string, Map<string, DatabaseItem>>} nodeMap key format likes 'server#db', children key format likes 'key#type'
*/ */
@ -87,8 +89,9 @@ const useConnectionStore = defineStore('connections', {
connections: [], // all connections connections: [], // all connections
serverStats: {}, // current server status info serverStats: {}, // current server status info
serverProfile: {}, // all server profile serverProfile: {}, // all server profile
keyFilter: {}, // all key filters in opened connections group by server+db
databases: {}, // all databases in opened connections group by server name databases: {}, // all databases in opened connections group by server name
nodeMap: {}, // all node in opened connections group by server+db and key+type nodeMap: {}, // all nodes in opened connections group by server#db and type/key
}), }),
getters: { getters: {
anyConnectionOpened() { anyConnectionOpened() {
@ -151,6 +154,7 @@ const useConnectionStore = defineStore('connections', {
markColor: conn.markColor, markColor: conn.markColor,
} }
} }
this.setKeyFilter(conn.name, -1, conn.defaultFilter)
} }
this.connections = conns this.connections = conns
this.serverProfile = profiles this.serverProfile = profiles
@ -333,8 +337,10 @@ const useConnectionStore = defineStore('connections', {
const dbs = this.databases[name] const dbs = this.databases[name]
for (const db of dbs) { for (const db of dbs) {
this.nodeMap[`${db.name}#${db.db}`]?.clear() this.removeKeyFilter(name, db.db)
this.nodeMap[`${name}#${db.db}`]?.clear()
} }
this.removeKeyFilter(name, -1)
delete this.databases[name] delete this.databases[name]
delete this.serverStats[name] delete this.serverStats[name]
@ -428,7 +434,8 @@ const useConnectionStore = defineStore('connections', {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async openDatabase(connName, db) { async openDatabase(connName, db) {
const { data, success, msg } = await OpenDatabase(connName, db) const filterPattern = this.getKeyFilter(connName, db)
const { data, success, msg } = await OpenDatabase(connName, db, filterPattern)
if (!success) { if (!success) {
throw new Error(msg) throw new Error(msg)
} }
@ -459,6 +466,20 @@ const useConnectionStore = defineStore('connections', {
this.nodeMap[`${connName}#${db}`]?.clear() this.nodeMap[`${connName}#${db}`]?.clear()
}, },
/**
* close database
* @param connName
* @param db
*/
closeDatabase(connName, db) {
const dbs = this.databases[connName]
delete dbs[db].children
dbs[db].isLeaf = false
dbs[db].opened = false
this.nodeMap[`${connName}#${db}`]?.clear()
},
/** /**
* *
* @param server * @param server
@ -1224,6 +1245,34 @@ const useConnectionStore = defineStore('connections', {
return [] return []
} }
}, },
/**
* get key filter pattern
* @param {string} server
* @param {number} db
* @returns {string}
*/
getKeyFilter(server, db) {
const key = `${server}#${db}`
if (!this.keyFilter.hasOwnProperty(key)) {
return this.keyFilter[`${server}#-1`] || '*'
}
return this.keyFilter[key] || '*'
},
/**
* set key filter
* @param {string} server
* @param {number} db
* @param {string} pattern
*/
setKeyFilter(server, db, pattern) {
this.keyFilter[`${server}#${db}`] = pattern || '*'
},
removeKeyFilter(server, db) {
this.keyFilter[`${server}#${db}`] = '*'
},
}, },
}) })

View File

@ -21,6 +21,13 @@ const useDialogStore = defineStore('dialog', {
}, },
newKeyDialogVisible: false, newKeyDialogVisible: false,
keyFilterParam: {
server: '',
db: 0,
pattern: '*',
},
keyFilterDialogVisible: false,
addFieldParam: { addFieldParam: {
server: '', server: '',
db: 0, db: 0,
@ -74,6 +81,26 @@ const useDialogStore = defineStore('dialog', {
this.groupDialogVisible = false this.groupDialogVisible = false
}, },
/**
*
* @param {string} server
* @param {number} db
* @param {string} pattern
*/
openKeyFilterDialog(server, db, pattern) {
this.keyFilterParam.server = server
this.keyFilterParam.db = db
this.keyFilterParam.pattern = '*'
this.keyFilterDialogVisible = true
},
closeKeyFilterDialog() {
this.keyFilterDialogVisible = false
},
/**
*
* @param {string} name
*/
openRenameGroupDialog(name) { openRenameGroupDialog(name) {
this.editGroup = name this.editGroup = name
this.groupDialogVisible = true this.groupDialogVisible = true