feat: add key type filter setting

perf: add closing connection status in edit connection dialog
This commit is contained in:
tiny-craft 2023-07-18 10:40:34 +08:00
parent 41e5ecfad2
commit c4f1a2e178
11 changed files with 240 additions and 156 deletions

View File

@ -347,24 +347,30 @@ 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, match string) (resp types.JSResp) { func (c *connectionService) OpenDatabase(connName string, db int, match string, keyType string) (resp types.JSResp) {
return c.ScanKeys(connName, db, match) return c.ScanKeys(connName, db, match, keyType)
} }
// ScanKeys scan all keys // ScanKeys scan all keys
func (c *connectionService) ScanKeys(connName string, db int, match string) (resp types.JSResp) { func (c *connectionService) ScanKeys(connName string, db int, match, keyType 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()
return return
} }
filterType := len(keyType) > 0
var keys []string var keys []string
//keys := map[string]keyItem{} //keys := map[string]keyItem{}
var cursor uint64 var cursor uint64
for { for {
var loadedKey []string var loadedKey []string
if filterType {
loadedKey, cursor, err = rdb.ScanType(ctx, cursor, match, 10000, keyType).Result()
} else {
loadedKey, cursor, err = rdb.Scan(ctx, cursor, match, 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

@ -1,6 +1,6 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { types, validType } from '../../consts/support_redis_type.js' import { typesColor, validType } from '../../consts/support_redis_type.js'
const props = defineProps({ const props = defineProps({
type: { type: {
@ -12,30 +12,22 @@ const props = defineProps({
}, },
color: { color: {
type: String, type: String,
default: '', default: 'white',
}, },
size: String, size: String,
}) })
const color = {
[types.STRING]: '#626aef',
[types.HASH]: '#576bfa',
[types.LIST]: '#34b285',
[types.SET]: '#bb7d52',
[types.ZSET]: '#d053a5',
}
const backgroundColor = computed(() => { const backgroundColor = computed(() => {
return color[props.type] return typesColor[props.type]
}) })
</script> </script>
<template> <template>
<n-tag <n-tag
:bordered="false" :bordered="false"
:color="{ color: backgroundColor, textColor: 'white' }" :color="{ color: backgroundColor, textColor: props.color }"
:size="props.size" :size="props.size"
class="redis-type-tag" :class="[props.size === 'small' ? 'redis-type-tag-small' : 'redis-type-tag']"
strong strong
> >
{{ props.type }} {{ props.type }}
@ -47,4 +39,8 @@ const backgroundColor = computed(() => {
.redis-type-tag { .redis-type-tag {
padding: 0 12px; padding: 0 12px;
} }
.redis-type-tag-small {
padding: 0 5px;
}
</style> </style>

View File

@ -24,7 +24,7 @@ const props = defineProps({
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const keyType = redisTypes.LIST const keyType = redisTypes.SET
const currentEditRow = ref({ const currentEditRow = ref({
no: 0, no: 0,
value: null, value: null,

View File

@ -9,7 +9,7 @@ import Close from '../icons/Close.vue'
import useConnectionStore from '../../stores/connections.js' import useConnectionStore from '../../stores/connections.js'
/** /**
* Dialog for create or edit connection * Dialog for new or edit connection
*/ */
const dialogStore = useDialog() const dialogStore = useDialog()
@ -29,6 +29,12 @@ const generalFormRules = () => {
} }
} }
const isEditMode = computed(() => !isEmpty(editName.value)) const isEditMode = computed(() => !isEmpty(editName.value))
const closingConnection = computed(() => {
if (isEmpty(editName.value)) {
return false
}
return connectionStore.isConnected(editName.value)
})
const groupOptions = computed(() => { const groupOptions = computed(() => {
const options = map(connectionStore.groups, (group) => ({ const options = map(connectionStore.groups, (group) => ({
@ -152,6 +158,7 @@ const onClose = () => {
preset="dialog" preset="dialog"
transform-origin="center" transform-origin="center"
> >
<n-spin :show="closingConnection">
<n-tabs v-model:value="tab" type="line" animated> <n-tabs v-model:value="tab" type="line" animated>
<n-tab-pane :tab="$t('general')" display-directive="show" name="general"> <n-tab-pane :tab="$t('general')" display-directive="show" name="general">
<n-form <n-form
@ -172,7 +179,12 @@ const onClose = () => {
<n-form-item :label="$t('conn_addr')" path="addr" required> <n-form-item :label="$t('conn_addr')" path="addr" required>
<n-input v-model:value="generalForm.addr" :placeholder="$t('conn_addr_tip')" /> <n-input v-model:value="generalForm.addr" :placeholder="$t('conn_addr_tip')" />
<n-text style="width: 40px; text-align: center">:</n-text> <n-text style="width: 40px; text-align: center">:</n-text>
<n-input-number v-model:value="generalForm.port" :max="65535" :min="1" style="width: 200px" /> <n-input-number
v-model:value="generalForm.port"
:max="65535"
:min="1"
style="width: 200px"
/>
</n-form-item> </n-form-item>
<n-form-item :label="$t('conn_pwd')" path="password"> <n-form-item :label="$t('conn_pwd')" path="password">
<n-input <n-input
@ -199,7 +211,10 @@ const onClose = () => {
label-placement="left" label-placement="left"
> >
<n-form-item :label="$t('conn_advn_filter')" path="defaultFilter"> <n-form-item :label="$t('conn_advn_filter')" path="defaultFilter">
<n-input v-model:value="generalForm.defaultFilter" :placeholder="$t('conn_advn_filter_tip')" /> <n-input
v-model:value="generalForm.defaultFilter"
:placeholder="$t('conn_advn_filter_tip')"
/>
</n-form-item> </n-form-item>
<n-form-item :label="$t('conn_advn_separator')" path="keySeparator"> <n-form-item :label="$t('conn_advn_separator')" path="keySeparator">
<n-input <n-input
@ -246,14 +261,17 @@ const onClose = () => {
<n-alert v-if="showTestConnFailResult" title="" type="error"> <n-alert v-if="showTestConnFailResult" title="" type="error">
{{ $t('conn_test_fail') }}: {{ testResult }} {{ $t('conn_test_fail') }}: {{ testResult }}
</n-alert> </n-alert>
</n-spin>
<template #action> <template #action>
<div class="flex-item-expand"> <div class="flex-item-expand">
<n-button :loading="testing" @click="onTestConnection">{{ $t('conn_test') }}</n-button> <n-button :loading="testing" :disabled="closingConnection" @click="onTestConnection">
{{ $t('conn_test') }}
</n-button>
</div> </div>
<div class="flex-item n-dialog__action"> <div class="flex-item n-dialog__action">
<n-button @click="onClose">{{ $t('cancel') }}</n-button> <n-button :disabled="closingConnection" @click="onClose">{{ $t('cancel') }}</n-button>
<n-button type="primary" @click="onSaveConnection"> <n-button type="primary" :disabled="closingConnection" @click="onSaveConnection">
{{ isEditMode ? $t('update') : $t('confirm') }} {{ isEditMode ? $t('update') : $t('confirm') }}
</n-button> </n-button>
</div> </div>

View File

@ -1,16 +1,29 @@
<script setup> <script setup>
import { reactive, ref, watch } from 'vue' import { computed, reactive, ref, watch } from 'vue'
import useDialog from '../../stores/dialog' import useDialog from '../../stores/dialog'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import useConnectionStore from '../../stores/connections.js' import useConnectionStore from '../../stores/connections.js'
import { types } from '../../consts/support_redis_type.js'
const i18n = useI18n() const i18n = useI18n()
const filterForm = reactive({ const filterForm = reactive({
server: '', server: '',
db: 0, db: 0,
type: '',
pattern: '', pattern: '',
}) })
const filterFormRef = ref(null) const filterFormRef = ref(null)
const typeOptions = computed(() => {
const options = Object.keys(types).map((t) => ({
value: t,
label: t,
}))
options.splice(0, 0, {
value: '',
label: i18n.t('all'),
})
return options
})
const formLabelWidth = '100px' const formLabelWidth = '100px'
const dialogStore = useDialog() const dialogStore = useDialog()
@ -18,9 +31,10 @@ watch(
() => dialogStore.keyFilterDialogVisible, () => dialogStore.keyFilterDialogVisible,
(visible) => { (visible) => {
if (visible) { if (visible) {
const { server, db, pattern } = dialogStore.keyFilterParam const { server, db, type, pattern } = dialogStore.keyFilterParam
filterForm.server = server filterForm.server = server
filterForm.db = db || 0 filterForm.db = db || 0
filterForm.type = type || ''
filterForm.pattern = pattern || '*' filterForm.pattern = pattern || '*'
} }
} }
@ -28,8 +42,8 @@ watch(
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
const onConfirm = () => { const onConfirm = () => {
const { server, db, pattern } = filterForm const { server, db, type, pattern } = filterForm
connectionStore.setKeyFilter(server, db, pattern) connectionStore.setKeyFilter(server, db, pattern, type)
connectionStore.reopenDatabase(server, db) connectionStore.reopenDatabase(server, db)
} }
@ -71,6 +85,9 @@ const onClose = () => {
<n-form-item :label="$t('db_index')" path="db"> <n-form-item :label="$t('db_index')" path="db">
<n-text>{{ filterForm.db }}</n-text> <n-text>{{ filterForm.db }}</n-text>
</n-form-item> </n-form-item>
<n-form-item :label="$t('type')" path="type" required>
<n-select v-model:value="filterForm.type" :options="typeOptions" />
</n-form-item>
<n-form-item :label="$t('filter_pattern')" required> <n-form-item :label="$t('filter_pattern')" required>
<n-input-group> <n-input-group>
<n-tooltip> <n-tooltip>

View File

@ -1,7 +1,7 @@
<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, NTag, useDialog, useMessage } from 'naive-ui' import { NIcon, NSpace, 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, remove } from 'lodash' import { get, indexOf, isEmpty, remove } from 'lodash'
@ -20,6 +20,7 @@ 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 Filter from '../icons/Filter.vue'
import Close from '../icons/Close.vue' import Close from '../icons/Close.vue'
import { typesColor } from '../../consts/support_redis_type.js'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -274,26 +275,51 @@ const renderLabel = ({ option }) => {
} }
const renderSuffix = ({ option }) => { const renderSuffix = ({ option }) => {
// return h(NButton,
// { text: true, type: 'primary' },
// { default: () => h(Key) })
if (option.type === ConnectionType.RedisDB) { if (option.type === ConnectionType.RedisDB) {
const { name: server, db } = option const { name: server, db } = option
const filterPattern = connectionStore.getKeyFilter(server, db) let { match: matchPattern, type: typeFilter } = connectionStore.getKeyFilter(server, db)
if (!isEmpty(filterPattern) && filterPattern !== '*') { const filterNodes = []
return h( // type filter tag
if (!isEmpty(typeFilter)) {
filterNodes.push(
h(
NTag,
{
size: 'small',
closable: true,
bordered: false,
color: { color: typesColor[typeFilter], textColor: 'white' },
onClose: () => {
// remove type filter
connectionStore.setKeyFilter(server, db, matchPattern)
connectionStore.reopenDatabase(server, db)
},
},
{ default: () => typeFilter }
)
)
}
// match pattern tag
if (!isEmpty(matchPattern) && matchPattern !== '*') {
filterNodes.push(
h(
NTag, NTag,
{ {
bordered: false, bordered: false,
closable: true, closable: true,
size: 'small', size: 'small',
onClose: () => { onClose: () => {
connectionStore.removeKeyFilter(server, db) // remove key match pattern
connectionStore.setKeyFilter(server, db, '*', typeFilter)
connectionStore.reopenDatabase(server, db) connectionStore.reopenDatabase(server, db)
}, },
}, },
{ default: () => filterPattern } { default: () => matchPattern }
) )
)
}
if (filterNodes.length > 0) {
return h(NSpace, { align: 'center', inline: true, size: 2 }, () => filterNodes)
} }
} }
return null return null
@ -379,8 +405,8 @@ const handleSelectContextMenu = (key) => {
dialogStore.openNewKeyDialog(redisKey, props.server, db) dialogStore.openNewKeyDialog(redisKey, props.server, db)
break break
case 'db_filter': case 'db_filter':
const pattern = connectionStore.getKeyFilter(props.server, db) const { match: pattern, type } = connectionStore.getKeyFilter(props.server, db)
dialogStore.openKeyFilterDialog(props.server, db, pattern) dialogStore.openKeyFilterDialog(props.server, db, pattern, type)
break break
case 'key_reload': case 'key_reload':
connectionStore.loadKeys(props.server, db, redisKey) connectionStore.loadKeys(props.server, db, redisKey)

View File

@ -241,11 +241,8 @@ const handleSelectContextMenu = (key) => {
// ask for close relevant connections before edit // ask for close relevant connections before edit
if (connectionStore.isConnected(name)) { if (connectionStore.isConnected(name)) {
confirmDialog.warning(i18n.t('edit_close_confirm'), () => { confirmDialog.warning(i18n.t('edit_close_confirm'), () => {
connectionStore.closeConnection(name).then((success) => { connectionStore.closeConnection(name)
if (success) {
dialogStore.openEditDialog(name) dialogStore.openEditDialog(name)
}
})
}) })
} else { } else {
dialogStore.openEditDialog(name) dialogStore.openEditDialog(name)
@ -285,7 +282,7 @@ const findSiblingsAndIndex = (node, nodes) => {
return [null, null] return [null, null]
} }
// delay save until stop drop after 2 seconds // delay save until drop stopped after 2 seconds
const saveSort = debounce(connectionStore.saveConnectionSorted, 2000, { trailing: true }) const saveSort = debounce(connectionStore.saveConnectionSorted, 2000, { trailing: true })
const handleDrop = ({ node, dragNode, dropPosition }) => { const handleDrop = ({ node, dragNode, dropPosition }) => {
const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections) const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)

View File

@ -6,6 +6,14 @@ export const types = {
ZSET: 'ZSET', ZSET: 'ZSET',
} }
export const typesColor = {
[types.STRING]: '#5A96E3',
[types.HASH]: '#9575DE',
[types.LIST]: '#7A9D54',
[types.SET]: '#F3AA60',
[types.ZSET]: '#FF6666',
}
// export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name])) // export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name]))
export const validType = (t) => { export const validType = (t) => {

View File

@ -149,8 +149,8 @@
"unit_day": "天", "unit_day": "天",
"unit_hour": "小时", "unit_hour": "小时",
"unit_minute": "分钟", "unit_minute": "分钟",
"all_info": "所有信息", "all_info": "全部信息",
"all": "所有", "all": "全部",
"launch_log": "运行日志", "launch_log": "运行日志",
"filter_server": "筛选服务器", "filter_server": "筛选服务器",
"filter_keyword": "筛选关键字", "filter_keyword": "筛选关键字",

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { endsWith, findIndex, get, isEmpty, size, split, uniq } from 'lodash' import { endsWith, findIndex, get, isEmpty, size, split, toUpper, uniq } from 'lodash'
import { import {
AddHashField, AddHashField,
AddListItem, AddListItem,
@ -31,6 +31,7 @@ import {
} from '../../wailsjs/go/services/connectionService.js' } from '../../wailsjs/go/services/connectionService.js'
import { ConnectionType } from '../consts/connection_type.js' import { ConnectionType } from '../consts/connection_type.js'
import useTabStore from './tab.js' import useTabStore from './tab.js'
import { types } from '../consts/support_redis_type.js'
const useConnectionStore = defineStore('connections', { const useConnectionStore = defineStore('connections', {
/** /**
@ -62,6 +63,7 @@ const useConnectionStore = defineStore('connections', {
* @property {Object} serverStats * @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, string>} keyFilter key is 'server#db', 'server#-1' stores default filter pattern
* @property {Object.<string, string>} typeFilter key is 'server#db'
* @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'
*/ */
@ -90,6 +92,7 @@ const useConnectionStore = defineStore('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 keyFilter: {}, // all key filters in opened connections group by server+db
typeFilter: {}, // all key type 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 nodes in opened connections group by server#db and type/key nodeMap: {}, // all nodes in opened connections group by server#db and type/key
}), }),
@ -434,8 +437,8 @@ const useConnectionStore = defineStore('connections', {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async openDatabase(connName, db) { async openDatabase(connName, db) {
const filterPattern = this.getKeyFilter(connName, db) const { match: filterPattern, type: keyType } = this.getKeyFilter(connName, db)
const { data, success, msg } = await OpenDatabase(connName, db, filterPattern) const { data, success, msg } = await OpenDatabase(connName, db, filterPattern, keyType)
if (!success) { if (!success) {
throw new Error(msg) throw new Error(msg)
} }
@ -1247,17 +1250,24 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* get key filter pattern * get key filter pattern and filter type
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @returns {string} * @returns {{match: string, type: string}}
*/ */
getKeyFilter(server, db) { getKeyFilter(server, db) {
let match, type
const key = `${server}#${db}` const key = `${server}#${db}`
if (!this.keyFilter.hasOwnProperty(key)) { if (!this.keyFilter.hasOwnProperty(key)) {
return this.keyFilter[`${server}#-1`] || '*' match = this.keyFilter[`${server}#-1`] || '*'
} else {
match = this.keyFilter[key] || '*'
}
type = this.typeFilter[`${server}#${db}`] || ''
return {
match,
type: toUpper(type),
} }
return this.keyFilter[key] || '*'
}, },
/** /**
@ -1265,13 +1275,16 @@ const useConnectionStore = defineStore('connections', {
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string} pattern * @param {string} pattern
* @param {string} [type]
*/ */
setKeyFilter(server, db, pattern) { setKeyFilter(server, db, pattern, type) {
this.keyFilter[`${server}#${db}`] = pattern || '*' this.keyFilter[`${server}#${db}`] = pattern || '*'
this.typeFilter[`${server}#${db}`] = types[toUpper(type)] || ''
}, },
removeKeyFilter(server, db) { removeKeyFilter(server, db) {
this.keyFilter[`${server}#${db}`] = '*' this.keyFilter[`${server}#${db}`] = '*'
delete this.typeFilter[`${server}#${db}`]
}, },
}, },
}) })

View File

@ -24,6 +24,7 @@ const useDialogStore = defineStore('dialog', {
keyFilterParam: { keyFilterParam: {
server: '', server: '',
db: 0, db: 0,
type: '',
pattern: '*', pattern: '*',
}, },
keyFilterDialogVisible: false, keyFilterDialogVisible: false,
@ -85,12 +86,14 @@ const useDialogStore = defineStore('dialog', {
* *
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string} pattern * @param {string} [pattern]
* @param {string} [type]
*/ */
openKeyFilterDialog(server, db, pattern) { openKeyFilterDialog(server, db, pattern, type) {
this.keyFilterParam.server = server this.keyFilterParam.server = server
this.keyFilterParam.db = db this.keyFilterParam.db = db
this.keyFilterParam.pattern = '*' this.keyFilterParam.type = type || ''
this.keyFilterParam.pattern = pattern || '*'
this.keyFilterDialogVisible = true this.keyFilterDialogVisible = true
}, },
closeKeyFilterDialog() { closeKeyFilterDialog() {