Compare commits

...

4 Commits

14 changed files with 228 additions and 120 deletions

View File

@ -46,6 +46,7 @@ type connectionItem struct {
cursor map[int]uint64 // current cursor of databases
entryCursor map[int]entryCursor // current entry cursor of databases
stepSize int64
db int // current database index
}
type browserService struct {
@ -288,10 +289,14 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
b.connMap[connName] = item
}
if db >= 0 {
if db >= 0 && item.db != db {
var rdb *redis.Client
if rdb, ok = client.(*redis.Client); ok && rdb != nil {
_ = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err()
if err = rdb.Do(item.ctx, "select", strconv.Itoa(db)).Err(); err != nil {
return
}
item.db = db
b.connMap[connName] = item
}
}
return
@ -299,21 +304,8 @@ func (b *browserService) getRedisClient(connName string, db int) (item connectio
// load current database size
func (b *browserService) loadDBSize(ctx context.Context, client redis.UniversalClient) int64 {
if cluster, isCluster := client.(*redis.ClusterClient); isCluster {
var keyCount atomic.Int64
cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
if size, serr := cli.DBSize(ctx).Result(); serr != nil {
return serr
} else {
keyCount.Add(size)
}
return nil
})
return keyCount.Load()
} else {
keyCount, _ := client.DBSize(ctx).Result()
return keyCount
}
keyCount, _ := client.DBSize(ctx).Result()
return keyCount
}
// save current scan cursor
@ -387,10 +379,10 @@ func (b *browserService) ServerInfo(name string) (resp types.JSResp) {
// OpenDatabase open select database, and list all keys
// @param path contain connection name and db name
func (b *browserService) OpenDatabase(connName string, db int, match string, keyType string) (resp types.JSResp) {
b.setClientCursor(connName, db, 0)
func (b *browserService) OpenDatabase(server string, db int) (resp types.JSResp) {
b.setClientCursor(server, db, 0)
item, err := b.getRedisClient(connName, db)
item, err := b.getRedisClient(server, db)
if err != nil {
resp.Msg = err.Error()
return
@ -516,6 +508,35 @@ func (b *browserService) LoadAllKeys(connName string, db int, match, keyType str
return
}
func (b *browserService) GetKeyType(param types.KeySummaryParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB)
if err != nil {
resp.Msg = err.Error()
return
}
client, ctx := item.client, item.ctx
key := strutil.DecodeRedisKey(param.Key)
var keyType string
keyType, err = client.Type(ctx, key).Result()
if err != nil {
resp.Msg = err.Error()
return
}
if keyType == "none" {
resp.Msg = "key not exists"
return
}
var data types.KeySummary
data.Type = strings.ToLower(keyType)
resp.Success = true
resp.Data = data
return
}
// GetKeySummary get key summary info
func (b *browserService) GetKeySummary(param types.KeySummaryParam) (resp types.JSResp) {
item, err := b.getRedisClient(param.Server, param.DB)

View File

@ -14,9 +14,9 @@ type KeySummaryParam struct {
type KeySummary struct {
Type string `json:"type"`
TTL int64 `json:"ttl"`
Size int64 `json:"size"`
Length int64 `json:"length"`
TTL int64 `json:"ttl,omitempty"`
Size int64 `json:"size,omitempty"`
Length int64 `json:"length,omitempty"`
}
type KeyDetailParam struct {

View File

@ -2,7 +2,7 @@
import ContentPane from './components/content/ContentPane.vue'
import BrowserPane from './components/sidebar/BrowserPane.vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { debounce, get } from 'lodash'
import { debounce } from 'lodash'
import { useThemeVars } from 'naive-ui'
import Ribbon from './components/sidebar/Ribbon.vue'
import ConnectionPane from './components/sidebar/ConnectionPane.vue'
@ -139,7 +139,7 @@ onMounted(async () => {
<div style="min-width: 68px; font-weight: 800">Tiny RDM</div>
<transition name="fade">
<n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px">
- {{ get(tabStore.currentTab, 'name') }}
- {{ tabStore.currentTabName }}
</n-text>
</transition>
</n-space>
@ -174,13 +174,15 @@ onMounted(async () => {
@update:size="handleResize">
<browser-pane
v-for="t in tabStore.tabs"
v-show="get(tabStore.currentTab, 'name') === t.name"
v-show="tabStore.currentTabName === t.name"
:key="t.name"
:db="t.db"
:server="t.name"
class="app-side flex-item-expand" />
</resizeable-wrapper>
<content-pane
v-for="t in tabStore.tabs"
v-show="get(tabStore.currentTab, 'name') === t.name"
v-show="tabStore.currentTabName === t.name"
:key="t.name"
:server="t.name"
class="flex-item-expand" />

View File

@ -3,6 +3,7 @@ import { computed, h } from 'vue'
import { useThemeVars } from 'naive-ui'
import { types, typesBgColor, typesColor } from '@/consts/support_redis_type.js'
import { get, map, toUpper } from 'lodash'
import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
const props = defineProps({
value: {
@ -23,15 +24,24 @@ const options = computed(() => {
const themeVars = useThemeVars()
const renderIcon = (option) => {
if (option.key === props.value) {
const backgroundColor = get(typesColor, option.key, themeVars.value.textColor3)
return h('div', { style: { borderRadius: '999px', width: '10px', height: '10px', backgroundColor } }, '')
}
return h(RedisTypeTag, {
type: option.key,
short: true,
size: 'small',
inverse: option.key === props.value,
})
}
const renderLabel = (option) => {
const color = get(typesColor, option.key, '')
return h('div', { style: { color, fontWeight: '450' } }, option.label)
return h(
'div',
{
style: {
fontWeight: option.key === props.value ? 'bold' : 'normal',
},
},
option.label,
)
}
const fontColor = computed(() => {

View File

@ -1,45 +1,69 @@
<script setup>
import { computed } from 'vue'
import { typesBgColor, typesColor, validType } from '@/consts/support_redis_type.js'
import { typesBgColor, typesColor, typesShortName } from '@/consts/support_redis_type.js'
import Binary from '@/components/icons/Binary.vue'
import { toUpper } from 'lodash'
const props = defineProps({
type: {
type: String,
validator(value) {
return validType(value)
},
default: 'STRING',
},
binaryKey: Boolean,
size: String,
short: Boolean,
round: Boolean,
inverse: Boolean,
})
const fontColor = computed(() => {
return typesColor[props.type]
if (props.inverse) {
return typesBgColor[props.type]
} else {
return typesColor[props.type]
}
})
const backgroundColor = computed(() => {
return typesBgColor[props.type]
if (props.inverse) {
return typesColor[props.type]
} else {
return typesBgColor[props.type]
}
})
const label = computed(() => {
if (props.short) {
return typesShortName[toUpper(props.type)] || 'N'
}
return toUpper(props.type)
})
</script>
<template>
<n-tag
:class="[props.size === 'small' ? 'redis-type-tag-small' : 'redis-type-tag']"
:class="{
'redis-type-tag-normal': !props.short && props.size !== 'small',
'redis-type-tag-small': !props.short && props.size === 'small',
'redis-type-tag-round': props.round,
}"
:color="{ color: backgroundColor, textColor: fontColor }"
:size="props.size"
bordered
strong>
{{ props.type }}
<b>{{ label }}</b>
<template #icon>
<n-icon v-if="binaryKey" :component="Binary" size="18" />
</template>
</n-tag>
<!-- <div class="redis-type-tag flex-box-h" :style="{backgroundColor: backgroundColor}">{{ props.type }}</div>-->
</template>
<style lang="scss">
.redis-type-tag {
.redis-type-tag-round {
border-radius: 9999px;
}
.redis-type-tag-normal {
padding: 0 12px;
}

View File

@ -119,7 +119,7 @@ const cleanHistory = async () => {
if (success) {
data.history = []
tableRef.value?.scrollTo({ top: 0 })
$message.success(i18n.t('common.success'))
$message.success(i18n.t('dialogue.handle_succ'))
}
} finally {
data.loading = false

View File

@ -87,7 +87,7 @@ const renderTypeLabel = (option) => {
default: () => [
h('div', {
style: {
borderRadius: '50%',
borderRadius: '9999px',
backgroundColor: typesColor[option.value],
width: '13px',
height: '13px',

View File

@ -3,8 +3,8 @@ import { useThemeVars } from 'naive-ui'
import BrowserTree from './BrowserTree.vue'
import IconButton from '@/components/common/IconButton.vue'
import useTabStore from 'stores/tab.js'
import { computed, nextTick, onMounted, reactive, ref, unref, watch } from 'vue'
import { find, get, map } from 'lodash'
import { computed, nextTick, onMounted, reactive, ref, unref } from 'vue'
import { find, map } from 'lodash'
import Refresh from '@/components/icons/Refresh.vue'
import useDialogStore from 'stores/dialog.js'
import { useI18n } from 'vue-i18n'
@ -24,29 +24,31 @@ import ListCheckbox from '@/components/icons/ListCheckbox.vue'
import Close from '@/components/icons/Close.vue'
import More from '@/components/icons/More.vue'
const props = defineProps({
server: String,
db: {
type: Number,
default: 0,
},
})
const themeVars = useThemeVars()
const i18n = useI18n()
const dialogStore = useDialogStore()
// const prefStore = usePreferencesStore()
const tabStore = useTabStore()
const browserStore = useBrowserStore()
const connectionStore = useConnectionStore()
const render = useRender()
const currentName = computed(() => get(tabStore.currentTab, 'name', ''))
const browserTreeRef = ref(null)
const loading = ref(false)
const fullyLoaded = ref(false)
const inCheckState = ref(false)
const checkedCount = ref(0)
const selectedDB = computed(() => {
return browserStore.selectedDatabases[currentName.value] || 0
})
const dbSelectOptions = computed(() => {
const dblist = browserStore.getDBList(currentName.value)
const dblist = browserStore.getDBList(props.server)
return map(dblist, (db) => {
if (selectedDB.value === db.db) {
if (props.db === db.db) {
return {
value: db.db,
label: `db${db.db} (${db.keys}/${db.maxKeys})`,
@ -71,7 +73,7 @@ const moreOptions = computed(() => {
})
const loadProgress = computed(() => {
const db = browserStore.getDatabase(currentName.value, selectedDB.value)
const db = browserStore.getDatabase(props.server, props.db)
if (db.maxKeys <= 0) {
return 100
}
@ -79,29 +81,29 @@ const loadProgress = computed(() => {
})
const checkedTip = computed(() => {
const dblist = browserStore.getDBList(currentName.value)
const db = find(dblist, { db: selectedDB.value })
const dblist = browserStore.getDBList(props.server)
const db = find(dblist, { db: props.db })
return `${checkedCount.value} / ${db.maxKeys}`
})
const onReload = async () => {
try {
loading.value = true
tabStore.setSelectedKeys(currentName.value)
const db = selectedDB.value
browserStore.closeDatabase(currentName.value, db)
browserTreeRef.value?.resetExpandKey(currentName.value, db)
tabStore.setSelectedKeys(props.server)
const db = props.db
browserStore.closeDatabase(props.server, db)
browserTreeRef.value?.resetExpandKey(props.server, db)
let matchType = unref(filterForm.type)
if (!types.hasOwnProperty(matchType)) {
matchType = ''
}
browserStore.setKeyFilter(currentName.value, {
browserStore.setKeyFilter(props.server, {
type: matchType,
pattern: unref(filterForm.pattern),
})
await browserStore.openDatabase(currentName.value, db)
fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, db)
await browserStore.openDatabase(props.server, db)
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db)
// $message.success(i18n.t('dialogue.reload_succ'))
} catch (e) {
console.warn(e)
@ -111,13 +113,13 @@ const onReload = async () => {
}
const onAddKey = () => {
dialogStore.openNewKeyDialog('', currentName.value, selectedDB.value)
dialogStore.openNewKeyDialog('', props.server, props.db)
}
const onLoadMore = async () => {
try {
loading.value = true
fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, selectedDB.value)
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, props.db)
} catch (e) {
$message.error(e.message)
} finally {
@ -128,7 +130,7 @@ const onLoadMore = async () => {
const onLoadAll = async () => {
try {
loading.value = true
await browserStore.loadAllKeys(currentName.value, selectedDB.value)
await browserStore.loadAllKeys(props.server, props.db)
fullyLoaded.value = true
} catch (e) {
$message.error(e.message)
@ -142,15 +144,30 @@ const onDeleteChecked = () => {
}
const onFlush = () => {
dialogStore.openFlushDBDialog(currentName.value, selectedDB.value)
dialogStore.openFlushDBDialog(props.server, props.db)
}
const onDisconnect = () => {
browserStore.closeConnection(currentName.value)
browserStore.closeConnection(props.server)
}
const handleSelectDB = async (db, prevDB) => {
// watch 'browserStore.openedDB[currentName.value]' instead
const handleSelectDB = async (db) => {
try {
loading.value = true
browserStore.closeDatabase(props.server, props.db)
browserStore.setKeyFilter(props.server, {})
await browserStore.openDatabase(props.server, db)
await nextTick()
await connectionStore.saveLastDB(props.server, db)
tabStore.upsertTab({ server: props.server, db })
// browserTreeRef.value?.resetExpandKey(props.server, db)
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db)
browserTreeRef.value?.refreshTree()
} catch (e) {
$message.error(e.message)
} finally {
loading.value = false
}
}
const filterForm = reactive({
@ -183,31 +200,6 @@ const onSelectOptions = (select) => {
}
}
watch(
() => browserStore.openedDB[currentName.value],
async (db, prevDB) => {
if (db === undefined) {
return
}
try {
loading.value = true
browserStore.closeDatabase(currentName.value, prevDB)
browserStore.setKeyFilter(currentName.value, {})
await browserStore.openDatabase(currentName.value, db)
// browserTreeRef.value?.resetExpandKey(currentName.value, db)
fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, db)
browserTreeRef.value?.refreshTree()
nextTick().then(() => connectionStore.saveLastDB(currentName.value, db))
} catch (e) {
$message.error(e.message)
} finally {
loading.value = false
}
},
)
onMounted(() => onReload())
// forbid dynamic switch key view due to performance issues
// const viewType = ref(0)
@ -220,7 +212,7 @@ onMounted(() => onReload())
<template>
<div class="nav-pane-container flex-box-v">
<!-- top function bar -->
<div class="flex-box-h nav-pane-func">
<div class="flex-box-h nav-pane-func" style="height: 36px">
<content-search-input
:debounce-wait="1000"
:full-search-icon="Search"
@ -271,11 +263,11 @@ onMounted(() => onReload())
ref="browserTreeRef"
v-model:checked-count="checkedCount"
:check-mode="inCheckState"
:db="browserStore.openedDB[currentName]"
:db="props.db"
:full-loaded="fullyLoaded"
:loading="loading && loadProgress <= 0"
:pattern="filterForm.filter"
:server="currentName" />
:server="props.server" />
<!-- bottom function bar -->
<div class="nav-pane-bottom flex-box-v">
<!-- <switch-button-->
@ -288,10 +280,10 @@ onMounted(() => onReload())
<transition mode="out-in" name="fade">
<div v-if="!inCheckState" class="flex-box-h nav-pane-func">
<n-select
v-model:value="browserStore.openedDB[currentName]"
:consistent-menu-width="false"
:filter="(pattern, option) => option.value.toString() === pattern"
:options="dbSelectOptions"
:value="props.db"
filterable
size="small"
style="min-width: 100px; max-width: 200px"
@ -372,16 +364,6 @@ onMounted(() => onReload())
border-color: #0000;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.nav-pane-top {
//@include bottom-shadow(0.1);
color: v-bind('themeVars.iconColor');

View File

@ -5,7 +5,7 @@ import { NIcon, NSpace, useThemeVars } from 'naive-ui'
import Key from '@/components/icons/Key.vue'
import Binary from '@/components/icons/Binary.vue'
import Database from '@/components/icons/Database.vue'
import { filter, find, get, includes, indexOf, isEmpty, map, remove, size, startsWith } from 'lodash'
import { filter, find, get, includes, indexOf, isEmpty, map, remove, size, startsWith, toUpper } from 'lodash'
import { useI18n } from 'vue-i18n'
import Refresh from '@/components/icons/Refresh.vue'
import CopyLink from '@/components/icons/CopyLink.vue'
@ -23,6 +23,7 @@ import LoadList from '@/components/icons/LoadList.vue'
import LoadAll from '@/components/icons/LoadAll.vue'
import useBrowserStore from 'stores/browser.js'
import { useRender } from '@/utils/render.js'
import RedisTypeTag from '@/components/common/RedisTypeTag.vue'
const props = defineProps({
server: String,
@ -364,13 +365,28 @@ const renderPrefix = ({ option }) => {
},
)
case ConnectionType.RedisValue:
return h(
NIcon,
{ size: 20 },
{
default: () => h(!!option.redisKeyCode ? Binary : Key),
},
)
if (option.redisType == null || option.redisType === 'loading') {
browserStore.loadKeyType({
server: props.server,
db: option.db,
key: option.redisKey,
keyCode: option.redisKeyCode,
})
// in loading
return h(
NIcon,
{ size: 20 },
{
default: () => h(!!option.redisKeyCode ? Binary : Key),
},
)
}
return h(RedisTypeTag, {
type: toUpper(option.redisType),
short: true,
size: 'small',
inverse: includes(selectedKeys.value, option.key),
})
}
}

View File

@ -301,6 +301,7 @@ const openConnection = async (name) => {
if (!isEmpty(connectingServer.value)) {
tabStore.upsertTab({
server: name,
db: browserStore.getSelectedDB(name),
})
}
} catch (e) {

View File

@ -11,6 +11,15 @@ export const types = {
STREAM: 'STREAM',
}
export const typesShortName = {
STRING: 'S',
HASH: 'H',
LIST: 'L',
SET: 'S',
ZSET: 'Z',
STREAM: 'X',
}
/**
* mark color for redis types
* @enum {string}

View File

@ -30,6 +30,7 @@ import {
GetCmdHistory,
GetKeyDetail,
GetKeySummary,
GetKeyType,
GetSlowLogs,
LoadAllKeys,
LoadNextKeys,
@ -72,6 +73,7 @@ const useBrowserStore = defineStore('browser', {
* @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
* @property {boolean} [expanded] - current node is expanded
* @property {DatabaseItem[]} [children]
* @property {string} [redisType] - redis type name, 'loading' indicate that is in loading progress
*/
/**
@ -112,7 +114,7 @@ const useBrowserStore = defineStore('browser', {
filter: {}, // all filters in opened connections map by server and FilterItem
loadingState: {}, // all loading state in opened connections map by server and LoadingState
viewType: {}, // view type selection for all opened connections group by 'server'
databases: {}, // all databases in opened connections group by 'server name'
databases: {}, // all database lists in opened connections group by 'server name'
nodeMap: {}, // all nodes in opened connections group by 'server#db' and 'type/key'
keySet: {}, // all keys set in opened connections group by 'server#db
openedDB: {}, // opened database map by server and database index
@ -339,6 +341,7 @@ const useBrowserStore = defineStore('browser', {
selDB.opened = true
selDB.maxKeys = maxKeys
this.openedDB[server] = db
set(this.loadingState, 'fullLoaded', end)
if (isEmpty(keys)) {
selDB.children = []
@ -465,6 +468,29 @@ const useBrowserStore = defineStore('browser', {
}
},
/**
* load key type
* @param {string} server
* @param {number} db
* @param {string} key
* @param {number[]} keyCode
* @return {Promise<void>}
*/
async loadKeyType({ server, db, key, keyCode }) {
try {
const nodeMap = this._getNodeMap(server, db)
const node = nodeMap.get(`${ConnectionType.RedisValue}/${key}`)
if (node == null || node.redisType != null) {
return
}
node.redisType = 'loading'
const { data } = await GetKeyType({ server, db, key: keyCode || key })
const { type } = data || {}
node.redisType = type
} finally {
}
},
/**
* reload key
* @param {string} server
@ -752,6 +778,7 @@ const useBrowserStore = defineStore('browser', {
keys: 0,
redisKey: k,
redisKeyCode: isBinaryKey ? key : undefined,
redisKeyType: undefined,
type: ConnectionType.RedisValue,
isLeaf: true,
}
@ -818,6 +845,7 @@ const useBrowserStore = defineStore('browser', {
keys: 0,
redisKey: handlePath,
redisKeyCode: isBinaryKey ? key : undefined,
redisKeyType: undefined,
type: ConnectionType.RedisValue,
isLeaf: true,
}

View File

@ -119,6 +119,10 @@ const useTabStore = defineStore('tab', {
// return current
},
currentTabName() {
return get(this.tabs, [this.activatedIndex, 'name'])
},
currentSelectedKeys() {
const tab = this.currentTab()
return get(tab, 'selectedKeys', [])

View File

@ -157,3 +157,14 @@ body {
.n-tabs .n-tabs-nav {
line-height: 1.3;
}
// animations
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}