feat: add key filter for database scan
This commit is contained in:
parent
ea2c24a5f9
commit
41e5ecfad2
|
@ -179,7 +179,7 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
|
|||
return
|
||||
}
|
||||
|
||||
// get total database
|
||||
// get total databases
|
||||
config, err := rdb.ConfigGet(ctx, "databases").Result()
|
||||
if err != nil {
|
||||
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
|
||||
// @param path contain connection name and db name
|
||||
func (c *connectionService) OpenDatabase(connName string, db int) (resp types.JSResp) {
|
||||
return c.ScanKeys(connName, db, "*")
|
||||
func (c *connectionService) OpenDatabase(connName string, db int, match string) (resp types.JSResp) {
|
||||
return c.ScanKeys(connName, db, match)
|
||||
}
|
||||
|
||||
// ScanKeys scan all keys below prefix
|
||||
func (c *connectionService) ScanKeys(connName string, db int, prefix string) (resp types.JSResp) {
|
||||
// ScanKeys scan all keys
|
||||
func (c *connectionService) ScanKeys(connName string, db int, match string) (resp types.JSResp) {
|
||||
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
|
@ -364,7 +364,7 @@ func (c *connectionService) ScanKeys(connName string, db int, prefix string) (re
|
|||
var cursor uint64
|
||||
for {
|
||||
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 {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
|
|
|
@ -17,6 +17,7 @@ import usePreferencesStore from './stores/preferences.js'
|
|||
import useConnectionStore from './stores/connections.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { darkTheme, lightTheme, useOsTheme } from 'naive-ui'
|
||||
import KeyFilterDialog from './components/dialogs/KeyFilterDialog.vue'
|
||||
|
||||
hljs.registerLanguage('json', json)
|
||||
hljs.registerLanguage('plaintext', plaintext)
|
||||
|
@ -95,6 +96,7 @@ const theme = computed(() => {
|
|||
<connection-dialog />
|
||||
<group-dialog />
|
||||
<new-key-dialog />
|
||||
<key-filter-dialog />
|
||||
<add-fields-dialog />
|
||||
<rename-key-dialog />
|
||||
<delete-key-dialog />
|
||||
|
|
|
@ -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>
|
|
@ -1,10 +1,10 @@
|
|||
<script setup>
|
||||
import { computed, h, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
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 ToggleDb from '../icons/ToggleDb.vue'
|
||||
import { get, indexOf, isEmpty } from 'lodash'
|
||||
import { get, indexOf, isEmpty, remove } from 'lodash'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Refresh from '../icons/Refresh.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 ToggleServer from '../icons/ToggleServer.vue'
|
||||
import Unlink from '../icons/Unlink.vue'
|
||||
import Filter from '../icons/Filter.vue'
|
||||
import Close from '../icons/Close.vue'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -86,6 +88,20 @@ const menuOptions = {
|
|||
label: i18n.t('new_key'),
|
||||
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 {
|
||||
return [
|
||||
|
@ -261,6 +277,26 @@ const renderSuffix = ({ option }) => {
|
|||
// return h(NButton,
|
||||
// { text: true, type: 'primary' },
|
||||
// { 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 }) => {
|
||||
|
@ -334,10 +370,18 @@ const handleSelectContextMenu = (key) => {
|
|||
case 'db_reload':
|
||||
connectionStore.reopenDatabase(props.server, db)
|
||||
break
|
||||
case 'db_close':
|
||||
remove(expandedKeys.value, (k) => k === `${props.server}/db${db}`)
|
||||
connectionStore.closeDatabase(props.server, db)
|
||||
break
|
||||
case 'db_newkey':
|
||||
case 'key_newkey':
|
||||
dialogStore.openNewKeyDialog(redisKey, props.server, db)
|
||||
break
|
||||
case 'db_filter':
|
||||
const pattern = connectionStore.getKeyFilter(props.server, db)
|
||||
dialogStore.openKeyFilterDialog(props.server, db, pattern)
|
||||
break
|
||||
case 'key_reload':
|
||||
connectionStore.loadKeys(props.server, db, redisKey)
|
||||
break
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
"view_as": "View As",
|
||||
"reload": "Reload",
|
||||
"open_connection": "Open Connection",
|
||||
"open_db": "Expand Database",
|
||||
"open_db": "Open Database",
|
||||
"close_db": "Close Database",
|
||||
"filter_key": "Filter Keys",
|
||||
"disconnect": "Disconnect",
|
||||
"dup_conn": "Duplicate Connection",
|
||||
|
@ -94,6 +95,10 @@
|
|||
"enter_elem": "Enter Element",
|
||||
"enter_member": "Enter Member",
|
||||
"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",
|
||||
"value": "Value",
|
||||
"field": "Field",
|
||||
|
|
|
@ -41,7 +41,8 @@
|
|||
"view_as": "查看方式",
|
||||
"reload": "重新载入",
|
||||
"open_connection": "打开连接",
|
||||
"open_db": "展开数据库",
|
||||
"open_db": "打开数据库",
|
||||
"close_db": "关闭数据库",
|
||||
"filter_key": "过滤键",
|
||||
"disconnect": "断开连接",
|
||||
"dup_conn": "复制连接",
|
||||
|
@ -97,6 +98,9 @@
|
|||
"enter_member": "输入成员",
|
||||
"enter_score": "输入分值",
|
||||
"element": "元素",
|
||||
"set_key_filter": "设置键过滤器",
|
||||
"filter_pattern": "过滤表达式",
|
||||
"filter_pattern_tip": "prefix_*:匹配以\"prefix_\"开头的键名\n*_suffix:匹配以\"_suffix\"结尾的键名\n*pattern*:匹配包含\"pattern\"的键名\nprefix_??:匹配以\"prefix_\"开头后跟两个任意字符的键名\n*abc*:匹配包含\"abc\"的任意位置的键名",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"field": "字段",
|
||||
|
|
|
@ -59,7 +59,9 @@ const useConnectionStore = defineStore('connections', {
|
|||
* @typedef {Object} ConnectionState
|
||||
* @property {string[]} groups
|
||||
* @property {ConnectionItem[]} connections
|
||||
* @property {Object} serverStats
|
||||
* @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, 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
|
||||
serverStats: {}, // current server status info
|
||||
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
|
||||
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: {
|
||||
anyConnectionOpened() {
|
||||
|
@ -151,6 +154,7 @@ const useConnectionStore = defineStore('connections', {
|
|||
markColor: conn.markColor,
|
||||
}
|
||||
}
|
||||
this.setKeyFilter(conn.name, -1, conn.defaultFilter)
|
||||
}
|
||||
this.connections = conns
|
||||
this.serverProfile = profiles
|
||||
|
@ -333,8 +337,10 @@ const useConnectionStore = defineStore('connections', {
|
|||
|
||||
const dbs = this.databases[name]
|
||||
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.serverStats[name]
|
||||
|
||||
|
@ -428,7 +434,8 @@ const useConnectionStore = defineStore('connections', {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
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) {
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
@ -459,6 +466,20 @@ const useConnectionStore = defineStore('connections', {
|
|||
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
|
||||
|
@ -1224,6 +1245,34 @@ const useConnectionStore = defineStore('connections', {
|
|||
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}`] = '*'
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -21,6 +21,13 @@ const useDialogStore = defineStore('dialog', {
|
|||
},
|
||||
newKeyDialogVisible: false,
|
||||
|
||||
keyFilterParam: {
|
||||
server: '',
|
||||
db: 0,
|
||||
pattern: '*',
|
||||
},
|
||||
keyFilterDialogVisible: false,
|
||||
|
||||
addFieldParam: {
|
||||
server: '',
|
||||
db: 0,
|
||||
|
@ -74,6 +81,26 @@ const useDialogStore = defineStore('dialog', {
|
|||
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) {
|
||||
this.editGroup = name
|
||||
this.groupDialogVisible = true
|
||||
|
|
Loading…
Reference in New Issue