Compare commits

...

5 Commits

14 changed files with 143 additions and 63 deletions

View File

@ -35,12 +35,12 @@
* Provides command-line operations. * Provides command-line operations.
* Provides slow logs. * Provides slow logs.
* Segmented loading and querying for List/Hash/Set/Sorted Set. * Segmented loading and querying for List/Hash/Set/Sorted Set.
* Decode/decompression display for value of List/Hash/Set/Sorted Set * Decode/decompression display for value of List/Hash/Set/Sorted Set.
* Inbuilt advanced editor - Monaco Editor.
## Roadmap ## Roadmap
- [ ] Real-time commands monitoring - [ ] Real-time commands monitoring
- [ ] Pub/Sub operations - [ ] Pub/Sub operations
- [ ] Embedding Monaco Editor
- [ ] Import/export connection profile - [ ] Import/export connection profile
- [ ] Import/export data - [ ] Import/export data

View File

@ -36,6 +36,7 @@
* 提供慢日志展示 * 提供慢日志展示
* List/Hash/Set/Sorted Set的分段加载和查询 * List/Hash/Set/Sorted Set的分段加载和查询
* List/Hash/Set/Sorted Set值的转码显示 * List/Hash/Set/Sorted Set值的转码显示
* 内置高级编辑器Monaco Editor
## 未来版本规划 ## 未来版本规划
- [ ] 命令实时监控 - [ ] 命令实时监控

View File

@ -51,7 +51,7 @@ type connectionItem struct {
type browserService struct { type browserService struct {
ctx context.Context ctx context.Context
connMap map[string]connectionItem connMap map[string]*connectionItem
cmdHistory []cmdHistoryItem cmdHistory []cmdHistoryItem
} }
@ -62,7 +62,7 @@ func Browser() *browserService {
if browser == nil { if browser == nil {
onceBrowser.Do(func() { onceBrowser.Do(func() {
browser = &browserService{ browser = &browserService{
connMap: map[string]connectionItem{}, connMap: map[string]*connectionItem{},
} }
}) })
} }
@ -80,21 +80,20 @@ func (b *browserService) Stop() {
item.client.Close() item.client.Close()
} }
} }
b.connMap = map[string]connectionItem{} b.connMap = map[string]*connectionItem{}
} }
// OpenConnection open redis server connection // OpenConnection open redis server connection
func (b *browserService) OpenConnection(name string) (resp types.JSResp) { func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
item, err := b.getRedisClient(name, 0) // get connection config
selConn := Connection().getConnection(name)
item, err := b.getRedisClient(name, selConn.LastDB)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
// get connection config
selConn := Connection().getConnection(name)
var totaldb int var totaldb int
if selConn.DBFilterType == "" || selConn.DBFilterType == "none" { if selConn.DBFilterType == "" || selConn.DBFilterType == "none" {
// get total databases // get total databases
@ -222,7 +221,7 @@ func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
// get a redis client from local cache or create a new open // get a redis client from local cache or create a new open
// if db >= 0, will also switch to db index // if db >= 0, will also switch to db index
func (b *browserService) getRedisClient(connName string, db int) (item connectionItem, err error) { func (b *browserService) getRedisClient(connName string, db int) (item *connectionItem, err error) {
var ok bool var ok bool
var client redis.UniversalClient var client redis.UniversalClient
if item, ok = b.connMap[connName]; ok { if item, ok = b.connMap[connName]; ok {
@ -275,7 +274,7 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
return return
} }
ctx, cancelFunc := context.WithCancel(b.ctx) ctx, cancelFunc := context.WithCancel(b.ctx)
item = connectionItem{ item = &connectionItem{
client: client, client: client,
ctx: ctx, ctx: ctx,
cancelFunc: cancelFunc, cancelFunc: cancelFunc,
@ -289,14 +288,19 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
b.connMap[connName] = item b.connMap[connName] = item
} }
if db >= 0 && item.db != db { // BUG: go-redis might not be executing commands on the corresponding database
// requiring a database switch with each command
if db >= 0 /*&& item.db != db*/ {
var rdb *redis.Client var rdb *redis.Client
if rdb, ok = client.(*redis.Client); ok && rdb != nil { if rdb, ok = client.(*redis.Client); ok && rdb != nil {
if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil { _, err = rdb.Pipelined(item.ctx, func(pipe redis.Pipeliner) error {
return pipe.Select(item.ctx, db).Err()
})
if err != nil {
return return
} }
item.db = db item.db = db
b.connMap[connName] = item b.connMap[connName].db = db
} }
} }
return return
@ -358,7 +362,7 @@ func (b *browserService) parseDBItemInfo(info string) map[string]int {
// ServerInfo get server info // ServerInfo get server info
func (b *browserService) ServerInfo(name string) (resp types.JSResp) { func (b *browserService) ServerInfo(name string) (resp types.JSResp) {
item, err := b.getRedisClient(name, 0) item, err := b.getRedisClient(name, -1)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return

View File

@ -1 +1 @@
d5ec7e0103cfa8c99bc40c20ffdcb4fb d5ec7e0103cfa8c99bc40c20ffdcb4fb

View File

@ -3,6 +3,8 @@ import { computed } from 'vue'
import { typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js' import { typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js'
import Binary from '@/components/icons/Binary.vue' import Binary from '@/components/icons/Binary.vue'
import { get, toUpper } from 'lodash' import { get, toUpper } from 'lodash'
import { useThemeVars } from 'naive-ui'
import Loading from '@/components/icons/Loading.vue'
const props = defineProps({ const props = defineProps({
type: { type: {
@ -20,21 +22,24 @@ const props = defineProps({
}, },
round: Boolean, round: Boolean,
inverse: Boolean, inverse: Boolean,
loading: Boolean,
}) })
const themeVars = useThemeVars()
const fontColor = computed(() => { const fontColor = computed(() => {
if (props.inverse) { if (props.inverse) {
return typesBgColor[props.type] return props.loading ? themeVars.value.tagColor : typesBgColor[props.type]
} else { } else {
return typesColor[props.type] return props.loading ? themeVars.value.textColorBase : typesColor[props.type]
} }
}) })
const backgroundColor = computed(() => { const backgroundColor = computed(() => {
if (props.inverse) { if (props.inverse) {
return typesColor[props.type] return props.loading ? themeVars.value.textColorBase : typesColor[props.type]
} else { } else {
return typesBgColor[props.type] return props.loading ? themeVars.value.tagColor : typesBgColor[props.type]
} }
}) })
@ -49,6 +54,7 @@ const label = computed(() => {
<template> <template>
<div <div
v-if="props.point" v-if="props.point"
:class="{ 'redis-type-tag-loading': props.loading }"
:style="{ :style="{
backgroundColor: fontColor, backgroundColor: fontColor,
width: Math.max(props.pointSize, 5) + 'px', width: Math.max(props.pointSize, 5) + 'px',
@ -61,12 +67,17 @@ const label = computed(() => {
'redis-type-tag-normal': !props.short && props.size !== 'small', 'redis-type-tag-normal': !props.short && props.size !== 'small',
'redis-type-tag-small': !props.short && props.size === 'small', 'redis-type-tag-small': !props.short && props.size === 'small',
'redis-type-tag-round': props.round, 'redis-type-tag-round': props.round,
'redis-type-tag-loading': props.loading,
}" }"
:color="{ color: backgroundColor, textColor: fontColor }" :color="{ color: backgroundColor, textColor: fontColor }"
:size="props.size" :size="props.size"
bordered bordered
strong> strong>
<b>{{ label }}</b> <b v-if="!props.loading">{{ label }}</b>
<n-icon v-else-if="props.short" size="14">
<loading stroke-width="4" />
</n-icon>
<b v-else>LOADING</b>
<template #icon> <template #icon>
<n-icon v-if="binaryKey" :component="Binary" size="18" /> <n-icon v-if="binaryKey" :component="Binary" size="18" />
</template> </template>
@ -85,4 +96,20 @@ const label = computed(() => {
.redis-type-tag-small { .redis-type-tag-small {
padding: 0 5px; padding: 0 5px;
} }
.redis-type-tag-loading {
animation: fadeInOut 2s infinite;
}
@keyframes fadeInOut {
0% {
opacity: 0.4;
}
50% {
opacity: 1;
}
100% {
opacity: 0.4;
}
}
</style> </style>

View File

@ -0,0 +1,51 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M7 4H41"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M7 44H41"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M11 44C13.6667 30.6611 18 23.9944 24 24C30 24.0056 34.3333 30.6722 37 44H11Z"
fill="none"
stroke="currentColor"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M37 4C34.3333 17.3389 30 24.0056 24 24C18 23.9944 13.6667 17.3278 11 4H37Z"
fill="none"
stroke="currentColor"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M21 15H27"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M19 38H29"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -154,8 +154,8 @@ const onDisconnect = () => {
const handleSelectDB = async (db) => { const handleSelectDB = async (db) => {
try { try {
loading.value = true loading.value = true
browserStore.closeDatabase(props.server, props.db)
browserStore.setKeyFilter(props.server, {}) browserStore.setKeyFilter(props.server, {})
browserStore.closeDatabase(props.server, props.db)
await browserStore.openDatabase(props.server, db) await browserStore.openDatabase(props.server, db)
await nextTick() await nextTick()
await connectionStore.saveLastDB(props.server, db) await connectionStore.saveLastDB(props.server, db)
@ -163,6 +163,7 @@ const handleSelectDB = async (db) => {
// browserTreeRef.value?.resetExpandKey(props.server, db) // browserTreeRef.value?.resetExpandKey(props.server, db)
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db) fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db)
browserTreeRef.value?.refreshTree() browserTreeRef.value?.refreshTree()
tabStore.setSelectedKeys(props.server)
} catch (e) { } catch (e) {
$message.error(e.message) $message.error(e.message)
} finally { } finally {

View File

@ -379,21 +379,14 @@ const renderPrefix = ({ option }) => {
}, },
) )
} }
if (option.redisType == null || option.redisType === 'loading') { const loading = isEmpty(option.redisType) || option.redisType === 'loading'
if (loading) {
browserStore.loadKeyType({ browserStore.loadKeyType({
server: props.server, server: props.server,
db: option.db, db: option.db,
key: option.redisKey, key: option.redisKey,
keyCode: option.redisKeyCode, keyCode: option.redisKeyCode,
}) })
// in loading
return h(
NIcon,
{ size: 20 },
{
default: () => h(!!option.redisKeyCode ? Binary : Key),
},
)
} }
switch (prefStore.keyIconType) { switch (prefStore.keyIconType) {
case typesIconStyle.FULL: case typesIconStyle.FULL:
@ -402,12 +395,14 @@ const renderPrefix = ({ option }) => {
short: false, short: false,
size: 'small', size: 'small',
inverse: includes(selectedKeys.value, option.key), inverse: includes(selectedKeys.value, option.key),
loading,
}) })
case typesIconStyle.POINT: case typesIconStyle.POINT:
return h(RedisTypeTag, { return h(RedisTypeTag, {
type: toUpper(option.redisType), type: toUpper(option.redisType),
point: true, point: true,
loading,
}) })
case typesIconStyle.SHORT: case typesIconStyle.SHORT:
@ -416,6 +411,7 @@ const renderPrefix = ({ option }) => {
type: toUpper(option.redisType), type: toUpper(option.redisType),
short: true, short: true,
size: 'small', size: 'small',
loading,
inverse: includes(selectedKeys.value, option.key), inverse: includes(selectedKeys.value, option.key),
}) })
} }

View File

@ -157,6 +157,7 @@ const exThemeVars = computed(() => {
background-color: v-bind('exThemeVars.ribbonColor'); background-color: v-bind('exThemeVars.ribbonColor');
box-sizing: border-box; box-sizing: border-box;
color: v-bind('themeVars.textColor2'); color: v-bind('themeVars.textColor2');
--wails-draggable: drag;
.ribbon-wrapper { .ribbon-wrapper {
gap: 2px; gap: 2px;
@ -165,6 +166,7 @@ const exThemeVars = computed(() => {
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
padding-right: 3px; padding-right: 3px;
--wails-draggable: none;
.ribbon-item { .ribbon-item {
width: 100%; width: 100%;
@ -219,6 +221,7 @@ const exThemeVars = computed(() => {
align-items: center; align-items: center;
padding: 10px 0; padding: 10px 0;
gap: 15px; gap: 15px;
--wails-draggable: none;
.nav-menu-button { .nav-menu-button {
margin-bottom: 6px; margin-bottom: 6px;

View File

@ -3,9 +3,9 @@ import {
endsWith, endsWith,
find, find,
get, get,
initial,
isEmpty, isEmpty,
join, join,
last,
map, map,
remove, remove,
set, set,
@ -477,16 +477,22 @@ const useBrowserStore = defineStore('browser', {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async loadKeyType({ server, db, key, keyCode }) { async loadKeyType({ server, db, key, keyCode }) {
const nodeMap = this._getNodeMap(server, db)
const node = nodeMap.get(`${ConnectionType.RedisValue}/${key}`)
if (node == null || !isEmpty(node.redisType)) {
return
}
try { try {
const nodeMap = this._getNodeMap(server, db)
const node = nodeMap.get(`${ConnectionType.RedisValue}/${key}`)
if (node == null || node.redisType != null) {
return
}
node.redisType = 'loading' node.redisType = 'loading'
const { data } = await GetKeyType({ server, db, key: keyCode || key }) const { data, success } = await GetKeyType({ server, db, key: keyCode || key })
const { type } = data || {} if (success) {
node.redisType = type const { type } = data || {}
node.redisType = type
} else {
node.redisType = 'NONE'
}
} catch (e) {
node.redisType = 'NONE'
} finally { } finally {
} }
}, },
@ -1706,29 +1712,20 @@ const useBrowserStore = defineStore('browser', {
/** /**
* *
* @param {string} connName * @param {string} server
* @param {number} db * @param {number} db
* @param {string} key * @param {string} key
* @param {string} newKey * @param {string} newKey
* @private * @private
*/ */
_renameKeyNode(connName, db, key, newKey) { _renameKeyNode(server, db, key, newKey) {
const nodeMap = this._getNodeMap(connName, db) this._deleteKeyNode(server, db, key, false)
const nodeKey = `${ConnectionType.RedisValue}/${key}` const { success } = this._addKeyNodes(server, db, [newKey])
const newNodeKey = `${ConnectionType.RedisValue}/${newKey}`
const node = nodeMap.get(nodeKey) if (success) {
if (node != null) { const separator = this._getSeparator(server)
// replace node map item const layer = initial(key.split(separator)).join(separator)
const separator = this._getSeparator(connName) this._tidyNode(server, db, layer)
node.label = last(split(newKey, separator))
node.key = `${connName}/db${db}#${newNodeKey}`
node.redisKey = newKey
nodeMap[newNodeKey] = node
nodeMap.delete(nodeKey)
// replace key set item
const keySet = this._getKeySet(connName, db)
keySet.delete(key)
keySet.add(newKey)
} }
}, },
@ -1940,18 +1937,18 @@ const useBrowserStore = defineStore('browser', {
/** /**
* rename key * rename key
* @param {string} connName * @param {string} server
* @param {number} db * @param {number} db
* @param {string} key * @param {string} key
* @param {string} newKey * @param {string} newKey
* @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: string}>} * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: string}>}
*/ */
async renameKey(connName, db, key, newKey) { async renameKey(server, db, key, newKey) {
const { success = false, msg } = await RenameKey(connName, db, key, newKey) const { success = false, msg } = await RenameKey(server, db, key, newKey)
if (success) { if (success) {
// delete old key and add new key struct // delete old key and add new key struct
this._renameKeyNode(connName, db, key, newKey) this._renameKeyNode(server, db, key, newKey)
return { success: true, nodeKey: `${connName}/db${db}#${ConnectionType.RedisValue}/${newKey}` } return { success: true, nodeKey: `${server}/db${db}#${ConnectionType.RedisValue}/${newKey}` }
} else { } else {
return { success: false, msg } return { success: false, msg }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 421 KiB