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
// @param path contain connection name and db name
func (c *connectionService) OpenDatabase(connName string, db int, match string) (resp types.JSResp) {
return c.ScanKeys(connName, db, match)
func (c *connectionService) OpenDatabase(connName string, db int, match string, keyType string) (resp types.JSResp) {
return c.ScanKeys(connName, db, match, keyType)
}
// 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)
if err != nil {
resp.Msg = err.Error()
return
}
filterType := len(keyType) > 0
var keys []string
//keys := map[string]keyItem{}
var cursor uint64
for {
var loadedKey []string
loadedKey, cursor, err = rdb.Scan(ctx, cursor, match, 10000).Result()
if filterType {
loadedKey, cursor, err = rdb.ScanType(ctx, cursor, match, 10000, keyType).Result()
} else {
loadedKey, cursor, err = rdb.Scan(ctx, cursor, match, 10000).Result()
}
if err != nil {
resp.Msg = err.Error()
return

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import Close from '../icons/Close.vue'
import useConnectionStore from '../../stores/connections.js'
/**
* Dialog for create or edit connection
* Dialog for new or edit connection
*/
const dialogStore = useDialog()
@ -29,6 +29,12 @@ const generalFormRules = () => {
}
}
const isEditMode = computed(() => !isEmpty(editName.value))
const closingConnection = computed(() => {
if (isEmpty(editName.value)) {
return false
}
return connectionStore.isConnected(editName.value)
})
const groupOptions = computed(() => {
const options = map(connectionStore.groups, (group) => ({
@ -152,108 +158,120 @@ const onClose = () => {
preset="dialog"
transform-origin="center"
>
<n-tabs v-model:value="tab" type="line" animated>
<n-tab-pane :tab="$t('general')" display-directive="show" name="general">
<n-form
ref="generalFormRef"
:label-width="formLabelWidth"
:model="generalForm"
:rules="generalFormRules()"
:show-require-mark="false"
label-align="right"
label-placement="left"
>
<n-form-item :label="$t('conn_name')" path="name" required>
<n-input v-model:value="generalForm.name" :placeholder="$t('conn_name_tip')" />
</n-form-item>
<n-form-item v-if="!isEditMode" :label="$t('conn_group')" required>
<n-select v-model:value="generalForm.group" :options="groupOptions" />
</n-form-item>
<n-form-item :label="$t('conn_addr')" path="addr" required>
<n-input v-model:value="generalForm.addr" :placeholder="$t('conn_addr_tip')" />
<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-form-item>
<n-form-item :label="$t('conn_pwd')" path="password">
<n-input
v-model:value="generalForm.password"
:placeholder="$t('conn_pwd_tip')"
show-password-on="click"
type="password"
/>
</n-form-item>
<n-form-item :label="$t('conn_usr')" path="username">
<n-input v-model="generalForm.username" :placeholder="$t('conn_usr_tip')" />
</n-form-item>
</n-form>
</n-tab-pane>
<n-spin :show="closingConnection">
<n-tabs v-model:value="tab" type="line" animated>
<n-tab-pane :tab="$t('general')" display-directive="show" name="general">
<n-form
ref="generalFormRef"
:label-width="formLabelWidth"
:model="generalForm"
:rules="generalFormRules()"
:show-require-mark="false"
label-align="right"
label-placement="left"
>
<n-form-item :label="$t('conn_name')" path="name" required>
<n-input v-model:value="generalForm.name" :placeholder="$t('conn_name_tip')" />
</n-form-item>
<n-form-item v-if="!isEditMode" :label="$t('conn_group')" required>
<n-select v-model:value="generalForm.group" :options="groupOptions" />
</n-form-item>
<n-form-item :label="$t('conn_addr')" path="addr" required>
<n-input v-model:value="generalForm.addr" :placeholder="$t('conn_addr_tip')" />
<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-form-item>
<n-form-item :label="$t('conn_pwd')" path="password">
<n-input
v-model:value="generalForm.password"
:placeholder="$t('conn_pwd_tip')"
show-password-on="click"
type="password"
/>
</n-form-item>
<n-form-item :label="$t('conn_usr')" path="username">
<n-input v-model="generalForm.username" :placeholder="$t('conn_usr_tip')" />
</n-form-item>
</n-form>
</n-tab-pane>
<n-tab-pane :tab="$t('advanced')" display-directive="show" name="advanced">
<n-form
ref="advanceFormRef"
:label-width="formLabelWidth"
:model="generalForm"
:rules="generalFormRules()"
:show-require-mark="false"
label-align="right"
label-placement="left"
>
<n-form-item :label="$t('conn_advn_filter')" path="defaultFilter">
<n-input v-model:value="generalForm.defaultFilter" :placeholder="$t('conn_advn_filter_tip')" />
</n-form-item>
<n-form-item :label="$t('conn_advn_separator')" path="keySeparator">
<n-input
v-model:value="generalForm.keySeparator"
:placeholder="$t('conn_advn_separator_tip')"
/>
</n-form-item>
<n-form-item :label="$t('conn_advn_conn_timeout')" path="connTimeout">
<n-input-number v-model:value="generalForm.connTimeout" :max="999999" :min="1">
<template #suffix>
{{ $t('second') }}
</template>
</n-input-number>
</n-form-item>
<n-form-item :label="$t('conn_advn_exec_timeout')" path="execTimeout">
<n-input-number v-model:value="generalForm.execTimeout" :max="999999" :min="1">
<template #suffix>
{{ $t('second') }}
</template>
</n-input-number>
</n-form-item>
<n-form-item :label="$t('conn_advn_mark_color')" path="markColor">
<div
v-for="color in predefineColors"
:key="color"
:class="{
'color-preset-item_selected': generalForm.markColor === color,
}"
:style="{ backgroundColor: color }"
class="color-preset-item"
@click="generalForm.markColor = color"
>
<n-icon v-if="color === ''" :component="Close" size="24" />
</div>
</n-form-item>
</n-form>
</n-tab-pane>
</n-tabs>
<n-tab-pane :tab="$t('advanced')" display-directive="show" name="advanced">
<n-form
ref="advanceFormRef"
:label-width="formLabelWidth"
:model="generalForm"
:rules="generalFormRules()"
:show-require-mark="false"
label-align="right"
label-placement="left"
>
<n-form-item :label="$t('conn_advn_filter')" path="defaultFilter">
<n-input
v-model:value="generalForm.defaultFilter"
:placeholder="$t('conn_advn_filter_tip')"
/>
</n-form-item>
<n-form-item :label="$t('conn_advn_separator')" path="keySeparator">
<n-input
v-model:value="generalForm.keySeparator"
:placeholder="$t('conn_advn_separator_tip')"
/>
</n-form-item>
<n-form-item :label="$t('conn_advn_conn_timeout')" path="connTimeout">
<n-input-number v-model:value="generalForm.connTimeout" :max="999999" :min="1">
<template #suffix>
{{ $t('second') }}
</template>
</n-input-number>
</n-form-item>
<n-form-item :label="$t('conn_advn_exec_timeout')" path="execTimeout">
<n-input-number v-model:value="generalForm.execTimeout" :max="999999" :min="1">
<template #suffix>
{{ $t('second') }}
</template>
</n-input-number>
</n-form-item>
<n-form-item :label="$t('conn_advn_mark_color')" path="markColor">
<div
v-for="color in predefineColors"
:key="color"
:class="{
'color-preset-item_selected': generalForm.markColor === color,
}"
:style="{ backgroundColor: color }"
class="color-preset-item"
@click="generalForm.markColor = color"
>
<n-icon v-if="color === ''" :component="Close" size="24" />
</div>
</n-form-item>
</n-form>
</n-tab-pane>
</n-tabs>
<!-- test result alert-->
<n-alert v-if="showTestConnSuccResult" title="" type="success">
{{ $t('conn_test_succ') }}
</n-alert>
<n-alert v-if="showTestConnFailResult" title="" type="error">
{{ $t('conn_test_fail') }}: {{ testResult }}
</n-alert>
<!-- test result alert-->
<n-alert v-if="showTestConnSuccResult" title="" type="success">
{{ $t('conn_test_succ') }}
</n-alert>
<n-alert v-if="showTestConnFailResult" title="" type="error">
{{ $t('conn_test_fail') }}: {{ testResult }}
</n-alert>
</n-spin>
<template #action>
<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 class="flex-item n-dialog__action">
<n-button @click="onClose">{{ $t('cancel') }}</n-button>
<n-button type="primary" @click="onSaveConnection">
<n-button :disabled="closingConnection" @click="onClose">{{ $t('cancel') }}</n-button>
<n-button type="primary" :disabled="closingConnection" @click="onSaveConnection">
{{ isEditMode ? $t('update') : $t('confirm') }}
</n-button>
</div>

View File

@ -1,16 +1,29 @@
<script setup>
import { reactive, ref, watch } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
import useDialog from '../../stores/dialog'
import { useI18n } from 'vue-i18n'
import useConnectionStore from '../../stores/connections.js'
import { types } from '../../consts/support_redis_type.js'
const i18n = useI18n()
const filterForm = reactive({
server: '',
db: 0,
type: '',
pattern: '',
})
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 dialogStore = useDialog()
@ -18,9 +31,10 @@ watch(
() => dialogStore.keyFilterDialogVisible,
(visible) => {
if (visible) {
const { server, db, pattern } = dialogStore.keyFilterParam
const { server, db, type, pattern } = dialogStore.keyFilterParam
filterForm.server = server
filterForm.db = db || 0
filterForm.type = type || ''
filterForm.pattern = pattern || '*'
}
}
@ -28,8 +42,8 @@ watch(
const connectionStore = useConnectionStore()
const onConfirm = () => {
const { server, db, pattern } = filterForm
connectionStore.setKeyFilter(server, db, pattern)
const { server, db, type, pattern } = filterForm
connectionStore.setKeyFilter(server, db, pattern, type)
connectionStore.reopenDatabase(server, db)
}
@ -71,6 +85,9 @@ const onClose = () => {
<n-form-item :label="$t('db_index')" path="db">
<n-text>{{ filterForm.db }}</n-text>
</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-input-group>
<n-tooltip>

View File

@ -1,7 +1,7 @@
<script setup>
import { computed, h, nextTick, onMounted, reactive, ref } from 'vue'
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 ToggleDb from '../icons/ToggleDb.vue'
import { get, indexOf, isEmpty, remove } from 'lodash'
@ -20,6 +20,7 @@ import ToggleServer from '../icons/ToggleServer.vue'
import Unlink from '../icons/Unlink.vue'
import Filter from '../icons/Filter.vue'
import Close from '../icons/Close.vue'
import { typesColor } from '../../consts/support_redis_type.js'
const props = defineProps({
server: String,
@ -274,27 +275,52 @@ const renderLabel = ({ option }) => {
}
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)
let { match: matchPattern, type: typeFilter } = connectionStore.getKeyFilter(server, db)
const filterNodes = []
// 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: () => filterPattern }
{ default: () => typeFilter }
)
)
}
// match pattern tag
if (!isEmpty(matchPattern) && matchPattern !== '*') {
filterNodes.push(
h(
NTag,
{
bordered: false,
closable: true,
size: 'small',
onClose: () => {
// remove key match pattern
connectionStore.setKeyFilter(server, db, '*', typeFilter)
connectionStore.reopenDatabase(server, db)
},
},
{ default: () => matchPattern }
)
)
}
if (filterNodes.length > 0) {
return h(NSpace, { align: 'center', inline: true, size: 2 }, () => filterNodes)
}
}
return null
}
@ -379,8 +405,8 @@ const handleSelectContextMenu = (key) => {
dialogStore.openNewKeyDialog(redisKey, props.server, db)
break
case 'db_filter':
const pattern = connectionStore.getKeyFilter(props.server, db)
dialogStore.openKeyFilterDialog(props.server, db, pattern)
const { match: pattern, type } = connectionStore.getKeyFilter(props.server, db)
dialogStore.openKeyFilterDialog(props.server, db, pattern, type)
break
case 'key_reload':
connectionStore.loadKeys(props.server, db, redisKey)

View File

@ -241,11 +241,8 @@ const handleSelectContextMenu = (key) => {
// ask for close relevant connections before edit
if (connectionStore.isConnected(name)) {
confirmDialog.warning(i18n.t('edit_close_confirm'), () => {
connectionStore.closeConnection(name).then((success) => {
if (success) {
dialogStore.openEditDialog(name)
}
})
connectionStore.closeConnection(name)
dialogStore.openEditDialog(name)
})
} else {
dialogStore.openEditDialog(name)
@ -285,7 +282,7 @@ const findSiblingsAndIndex = (node, nodes) => {
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 handleDrop = ({ node, dragNode, dropPosition }) => {
const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)

View File

@ -6,6 +6,14 @@ export const types = {
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 validType = (t) => {

View File

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

View File

@ -1,5 +1,5 @@
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 {
AddHashField,
AddListItem,
@ -31,6 +31,7 @@ import {
} from '../../wailsjs/go/services/connectionService.js'
import { ConnectionType } from '../consts/connection_type.js'
import useTabStore from './tab.js'
import { types } from '../consts/support_redis_type.js'
const useConnectionStore = defineStore('connections', {
/**
@ -62,6 +63,7 @@ const useConnectionStore = defineStore('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, string>} typeFilter key is 'server#db'
* @property {Object.<string, DatabaseItem[]>} databases
* @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
serverProfile: {}, // all server profile
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
nodeMap: {}, // all nodes in opened connections group by server#db and type/key
}),
@ -434,8 +437,8 @@ const useConnectionStore = defineStore('connections', {
* @returns {Promise<void>}
*/
async openDatabase(connName, db) {
const filterPattern = this.getKeyFilter(connName, db)
const { data, success, msg } = await OpenDatabase(connName, db, filterPattern)
const { match: filterPattern, type: keyType } = this.getKeyFilter(connName, db)
const { data, success, msg } = await OpenDatabase(connName, db, filterPattern, keyType)
if (!success) {
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 {number} db
* @returns {string}
* @returns {{match: string, type: string}}
*/
getKeyFilter(server, db) {
let match, type
const key = `${server}#${db}`
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 {number} db
* @param {string} pattern
* @param {string} [type]
*/
setKeyFilter(server, db, pattern) {
setKeyFilter(server, db, pattern, type) {
this.keyFilter[`${server}#${db}`] = pattern || '*'
this.typeFilter[`${server}#${db}`] = types[toUpper(type)] || ''
},
removeKeyFilter(server, db) {
this.keyFilter[`${server}#${db}`] = '*'
delete this.typeFilter[`${server}#${db}`]
},
},
})

View File

@ -24,6 +24,7 @@ const useDialogStore = defineStore('dialog', {
keyFilterParam: {
server: '',
db: 0,
type: '',
pattern: '*',
},
keyFilterDialogVisible: false,
@ -85,12 +86,14 @@ const useDialogStore = defineStore('dialog', {
*
* @param {string} server
* @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.db = db
this.keyFilterParam.pattern = '*'
this.keyFilterParam.type = type || ''
this.keyFilterParam.pattern = pattern || '*'
this.keyFilterDialogVisible = true
},
closeKeyFilterDialog() {