perf: move server status pane into browser pane, add server node to the top of database tree list

This commit is contained in:
tiny-craft 2023-07-14 16:17:59 +08:00
parent 484cdcf54e
commit 0bc8335eef
9 changed files with 164 additions and 125 deletions

View File

@ -129,7 +129,7 @@ func (c *connectionService) SaveSortedConnection(sortedConns types.Connections)
return return
} }
// CreateGroup create new group // CreateGroup create a new group
func (c *connectionService) CreateGroup(name string) (resp types.JSResp) { func (c *connectionService) CreateGroup(name string) (resp types.JSResp) {
err := c.conns.CreateGroup(name) err := c.conns.CreateGroup(name)
if err != nil { if err != nil {
@ -151,7 +151,7 @@ func (c *connectionService) RenameGroup(name, newName string) (resp types.JSResp
return return
} }
// DeleteGroup remove group by name // DeleteGroup remove a group by name
func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp types.JSResp) { func (c *connectionService) DeleteGroup(name string, includeConn bool) (resp types.JSResp) {
err := c.conns.DeleteGroup(name, includeConn) err := c.conns.DeleteGroup(name, includeConn)
if err != nil { if err != nil {

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { types } from '../../consts/support_redis_type.js' import { types } from '../../consts/support_redis_type.js'
import ContentValueHash from '../content_value/ContentValueHash.vue' import ContentValueHash from '../content_value/ContentValueHash.vue'
import ContentValueList from '../content_value/ContentValueList.vue' import ContentValueList from '../content_value/ContentValueList.vue'
@ -12,6 +12,35 @@ import { useDialog } from 'naive-ui'
import useConnectionStore from '../../stores/connections.js' import useConnectionStore from '../../stores/connections.js'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useConfirmDialog } from '../../utils/confirm_dialog.js' import { useConfirmDialog } from '../../utils/confirm_dialog.js'
import ContentServerStatus from '../content_value/ContentServerStatus.vue'
const serverInfo = ref({})
const autoRefresh = ref(false)
const serverName = computed(() => {
if (tabContent.value != null) {
return tabContent.value.name
}
return ''
})
const refreshInfo = async () => {
if (!isEmpty(serverName.value) && connectionStore.isConnected(serverName.value)) {
serverInfo.value = await connectionStore.getServerInfo(serverName.value)
}
}
let intervalId
onMounted(() => {
refreshInfo()
intervalId = setInterval(() => {
if (autoRefresh.value) {
refreshInfo()
}
}, 5000)
})
onUnmounted(() => {
clearInterval(intervalId)
})
const valueComponents = { const valueComponents = {
[types.STRING]: ContentValueString, [types.STRING]: ContentValueString,
@ -31,10 +60,6 @@ const tab = computed(() =>
})) }))
) )
/**
*
* @type {ComputedRef<TabItem>}
*/
const tabContent = computed(() => { const tabContent = computed(() => {
const tab = tabStore.currentTab const tab = tabStore.currentTab
if (tab == null) { if (tab == null) {
@ -50,6 +75,14 @@ const tabContent = computed(() => {
} }
}) })
const showServerStatus = computed(() => {
return tabContent.value == null || isEmpty(tabContent.value.keyPath)
})
const showNonexists = computed(() => {
return tabContent.value.value == null
})
const onUpdateValue = (tabIndex) => { const onUpdateValue = (tabIndex) => {
tabStore.switchTab(tabIndex) tabStore.switchTab(tabIndex)
} }
@ -95,10 +128,11 @@ const onCloseTab = (tabIndex) => {
</n-tabs> </n-tabs>
<!-- TODO: add loading status --> <!-- TODO: add loading status -->
<div v-if="tabContent == null || isEmpty(tabContent.keyPath)" class="flex-item-expand flex-box-v"> <div v-if="showServerStatus" class="content-container flex-item-expand flex-box-v">
<n-empty :description="$t('empty_tab_content')" class="empty-content" /> <!-- select nothing or select server node, display server status -->
<content-server-status v-model:auto-refresh="autoRefresh" :server="serverName" :info="serverInfo" />
</div> </div>
<div v-else-if="tabContent.value == null" class="flex-item-expand flex-box-v"> <div v-else-if="showNonexists" class="content-container flex-item-expand flex-box-v">
<n-empty :description="$t('nonexist_tab_content')" class="empty-content"> <n-empty :description="$t('nonexist_tab_content')" class="empty-content">
<template #extra> <template #extra>
<n-button @click="onReloadKey">{{ $t('reload') }}</n-button> <n-button @click="onReloadKey">{{ $t('reload') }}</n-button>
@ -119,6 +153,12 @@ const onCloseTab = (tabIndex) => {
<style lang="scss" scoped> <style lang="scss" scoped>
@import 'content'; @import 'content';
.content-container {
padding: 5px;
box-sizing: border-box;
}
//.tab-item { //.tab-item {
// gap: 5px; // gap: 5px;
// padding: 0 5px 0 10px; // padding: 0 5px 0 10px;

View File

@ -1,45 +1,14 @@
<script setup> <script setup>
import useDialog from '../../stores/dialog.js'
import AddLink from '../icons/AddLink.vue' import AddLink from '../icons/AddLink.vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { useDialog } from 'naive-ui'
import useConnectionStore from '../../stores/connections.js'
import { isEmpty } from 'lodash'
import ContentServerStatus from '../content_value/ContentServerStatus.vue'
const dialogStore = useDialog() const dialogStore = useDialog()
const connectionStore = useConnectionStore()
const serverInfo = ref({})
const autoRefresh = ref(true)
const refreshInfo = async () => {
const server = connectionStore.selectedServer
if (!isEmpty(server) && connectionStore.isConnected(server)) {
serverInfo.value = await connectionStore.getServerInfo(server)
}
}
let intervalId
onMounted(() => {
intervalId = setInterval(() => {
if (autoRefresh.value) {
refreshInfo()
}
}, 5000)
})
onUnmounted(() => {
clearInterval(intervalId)
})
watch(() => connectionStore.selectedServer, refreshInfo)
const hasContent = computed(() => !isEmpty(serverInfo.value))
</script> </script>
<template> <template>
<div class="content-container flex-box-v" :style="{ 'justify-content': hasContent ? 'flex-start' : 'center' }"> <div class="content-container flex-box-v">
<!-- TODO: replace icon to app icon --> <!-- TODO: replace icon to app icon -->
<n-empty v-if="!hasContent" :description="$t('empty_server_content')"> <n-empty :description="$t('empty_server_content')">
<template #extra> <template #extra>
<n-button @click="dialogStore.openNewDialog()"> <n-button @click="dialogStore.openNewDialog()">
<template #icon> <template #icon>
@ -49,12 +18,6 @@ const hasContent = computed(() => !isEmpty(serverInfo.value))
</n-button> </n-button>
</template> </template>
</n-empty> </n-empty>
<content-server-status
v-else
v-model:auto-refresh="autoRefresh"
:server="connectionStore.selectedServer"
:info="serverInfo"
/>
</div> </div>
</template> </template>
@ -62,7 +25,7 @@ const hasContent = computed(() => !isEmpty(serverInfo.value))
@import 'content'; @import 'content';
.content-container { .content-container {
//justify-content: center; justify-content: center;
padding: 5px; padding: 5px;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -123,8 +123,8 @@ const onFilterInfo = (val) => {
</n-gi> </n-gi>
<n-gi :span="6"> <n-gi :span="6">
<n-statistic :value="totalKeys"> <n-statistic :value="totalKeys">
<template #label <template #label>
>{{ $t('total_keys') }} {{ $t('total_keys') }}
<n-icon :component="Help" /> <n-icon :component="Help" />
</template> </template>
</n-statistic> </n-statistic>

View File

@ -1,10 +1,10 @@
<script setup> <script setup>
import { 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, useDialog, useMessage } from 'naive-ui' import { NIcon, 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 { indexOf, isEmpty } from 'lodash' import { get, indexOf, isEmpty } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Refresh from '../icons/Refresh.vue' import Refresh from '../icons/Refresh.vue'
import CopyLink from '../icons/CopyLink.vue' import CopyLink from '../icons/CopyLink.vue'
@ -16,15 +16,33 @@ import useDialogStore from '../../stores/dialog.js'
import { ClipboardSetText } from '../../../wailsjs/runtime/runtime.js' import { ClipboardSetText } from '../../../wailsjs/runtime/runtime.js'
import useConnectionStore from '../../stores/connections.js' import useConnectionStore from '../../stores/connections.js'
import { useConfirmDialog } from '../../utils/confirm_dialog.js' import { useConfirmDialog } from '../../utils/confirm_dialog.js'
import ToggleServer from '../icons/ToggleServer.vue'
import Unlink from '../icons/Unlink.vue'
const props = defineProps({
server: String,
})
const i18n = useI18n() const i18n = useI18n()
const loading = ref(false) const loading = ref(false)
const loadingConnections = ref(false) const loadingConnections = ref(false)
const expandedKeys = ref([]) const expandedKeys = ref([props.server])
const selectedKeys = ref([]) const selectedKeys = ref([props.server])
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const data = computed(() => {
const dbs = get(connectionStore.databases, props.server, [])
return [
{
key: props.server,
label: props.server,
type: ConnectionType.Server,
children: dbs,
},
]
})
const contextMenuParam = reactive({ const contextMenuParam = reactive({
show: false, show: false,
x: 0, x: 0,
@ -40,6 +58,21 @@ const renderIcon = (icon) => {
} }
} }
const menuOptions = { const menuOptions = {
[ConnectionType.Server]: () => {
console.log('open server context')
return [
{
key: 'server_reload',
label: i18n.t('reload'),
icon: renderIcon(Refresh),
},
{
key: 'server_close',
label: i18n.t('disconnect'),
icon: renderIcon(Unlink),
},
]
},
[ConnectionType.RedisDB]: ({ opened }) => { [ConnectionType.RedisDB]: ({ opened }) => {
if (opened) { if (opened) {
return [ return [
@ -127,10 +160,6 @@ onMounted(async () => {
} }
}) })
const props = defineProps({
server: String,
})
const expandKey = (key) => { const expandKey = (key) => {
const idx = indexOf(expandedKeys.value, key) const idx = indexOf(expandedKeys.value, key)
if (idx === -1) { if (idx === -1) {
@ -159,24 +188,37 @@ const onUpdateExpanded = (value, option, meta) => {
} }
const onUpdateSelectedKeys = (keys, options) => { const onUpdateSelectedKeys = (keys, options) => {
try {
if (!isEmpty(options)) { if (!isEmpty(options)) {
// prevent load duplicate key // prevent load duplicate key
for (const node of options) { for (const node of options) {
if (node.type === ConnectionType.RedisValue) { if (node.type === ConnectionType.RedisValue) {
const { key, name, db, redisKey } = node const { key, db, redisKey } = node
if (indexOf(selectedKeys.value, key) === -1) { if (indexOf(selectedKeys.value, key) === -1) {
connectionStore.loadKeyValue(name, db, redisKey) connectionStore.loadKeyValue(props.server, db, redisKey)
}
break
} }
return
} }
} }
// default is load blank key to display server status
connectionStore.loadKeyValue(props.server, 0)
}
} finally {
selectedKeys.value = keys selectedKeys.value = keys
}
} }
const renderPrefix = ({ option }) => { const renderPrefix = ({ option }) => {
switch (option.type) { switch (option.type) {
case ConnectionType.Server:
return h(
NIcon,
{ size: 20 },
{
default: () => h(ToggleServer, { modelValue: false }),
}
)
case ConnectionType.RedisDB: case ConnectionType.RedisDB:
return h( return h(
NIcon, NIcon,
@ -258,7 +300,7 @@ const onLoadTree = async (node) => {
case ConnectionType.RedisDB: case ConnectionType.RedisDB:
loading.value = true loading.value = true
try { try {
await connectionStore.openDatabase(node.name, node.db) await connectionStore.openDatabase(props.server, node.db)
} catch (e) { } catch (e) {
message.error(e.message) message.error(e.message)
node.isLeaf = undefined node.isLeaf = undefined
@ -276,30 +318,38 @@ const onLoadTree = async (node) => {
const confirmDialog = useConfirmDialog() const confirmDialog = useConfirmDialog()
const handleSelectContextMenu = (key) => { const handleSelectContextMenu = (key) => {
contextMenuParam.show = false contextMenuParam.show = false
const { name, db, key: nodeKey, redisKey } = contextMenuParam.currentNode const { db, key: nodeKey, redisKey } = contextMenuParam.currentNode
switch (key) { switch (key) {
case 'server_reload':
connectionStore.openConnection(props.server, true).then(() => {
message.success(i18n.t('reload_succ'))
})
break
case 'server_close':
connectionStore.closeConnection(props.server)
break
case 'db_open': case 'db_open':
nextTick().then(() => expandKey(nodeKey)) nextTick().then(() => expandKey(nodeKey))
break break
case 'db_reload': case 'db_reload':
connectionStore.reopenDatabase(name, db) connectionStore.reopenDatabase(props.server, db)
break break
case 'db_newkey': case 'db_newkey':
case 'key_newkey': case 'key_newkey':
dialogStore.openNewKeyDialog(redisKey, name, db) dialogStore.openNewKeyDialog(redisKey, props.server, db)
break break
case 'key_reload': case 'key_reload':
connectionStore.loadKeys(name, db, redisKey) connectionStore.loadKeys(props.server, db, redisKey)
break break
case 'value_reload': case 'value_reload':
connectionStore.loadKeyValue(name, db, redisKey) connectionStore.loadKeyValue(props.server, db, redisKey)
break break
case 'key_remove': case 'key_remove':
dialogStore.openDeleteKeyDialog(name, db, redisKey + ':*') dialogStore.openDeleteKeyDialog(props.server, db, redisKey + ':*')
break break
case 'value_remove': case 'value_remove':
confirmDialog.warning(i18n.t('remove_tip', { name: redisKey }), () => { confirmDialog.warning(i18n.t('remove_tip', { name: redisKey }), () => {
connectionStore.deleteKey(name, db, redisKey).then((success) => { connectionStore.deleteKey(props.server, db, redisKey).then((success) => {
if (success) { if (success) {
message.success(i18n.t('delete_key_succ', { key: redisKey })) message.success(i18n.t('delete_key_succ', { key: redisKey }))
} }
@ -334,7 +384,7 @@ const handleOutsideContextMenu = () => {
:block-node="true" :block-node="true"
:animated="false" :animated="false"
:cancelable="false" :cancelable="false"
:data="connectionStore.databases[props.server] || []" :data="data"
:expand-on-click="false" :expand-on-click="false"
:expanded-keys="expandedKeys" :expanded-keys="expandedKeys"
:selected-keys="selectedKeys" :selected-keys="selectedKeys"

View File

@ -27,18 +27,6 @@ const message = useMessage()
const expandedKeys = ref([]) const expandedKeys = ref([])
const selectedKeys = ref([]) const selectedKeys = ref([])
watch(selectedKeys, () => {
const key = selectedKeys.value[0]
// try to remove group name
const kparts = split(key, '/')
const len = size(kparts)
if (len > 1) {
connectionStore.selectedServer = kparts[len - 1]
} else {
connectionStore.selectedServer = selectedKeys.value[0]
}
})
const props = defineProps({ const props = defineProps({
filterPattern: { filterPattern: {
type: String, type: String,

View File

@ -124,7 +124,6 @@
"field_required": "This item should not be blank", "field_required": "This item should not be blank",
"spec_field_required": "\"{key}\" should not be blank", "spec_field_required": "\"{key}\" should not be blank",
"no_connections": "No Connection", "no_connections": "No Connection",
"empty_tab_content": "Select the key from left list to see detail.",
"nonexist_tab_content": "The selected key does not exist. Please retry", "nonexist_tab_content": "The selected key does not exist. Please retry",
"empty_server_content": "Connect server from left list", "empty_server_content": "Connect server from left list",
"reload_when_succ": "Reload immediately after success", "reload_when_succ": "Reload immediately after success",

View File

@ -127,7 +127,6 @@
"field_required": "此项不能为空", "field_required": "此项不能为空",
"spec_field_required": "{key} 不能为空", "spec_field_required": "{key} 不能为空",
"no_connections": "空空如也", "no_connections": "空空如也",
"empty_tab_content": "可以从左边选择键来查看键的详细内容",
"nonexist_tab_content": "所选键不存在,请尝试刷新重试", "nonexist_tab_content": "所选键不存在,请尝试刷新重试",
"empty_server_content": "可以从左边选择并打开连接", "empty_server_content": "可以从左边选择并打开连接",
"reload_when_succ": "操作成功后立即重新加载", "reload_when_succ": "操作成功后立即重新加载",

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { endsWith, findIndex, get, isEmpty, last, size, split, uniq } from 'lodash' import { endsWith, findIndex, get, isEmpty, size, split, uniq } from 'lodash'
import { import {
AddHashField, AddHashField,
AddListItem, AddListItem,
@ -71,7 +71,6 @@ const useConnectionStore = defineStore('connections', {
state: () => ({ state: () => ({
groups: [], // all group name set groups: [], // all group name set
connections: [], // all connections connections: [], // all connections
selectedServer: '', // current selected server
serverStats: {}, // current server status info serverStats: {}, // current server status info
databases: {}, // all databases in opened connections group by server name 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 node in opened connections group by server+db and key+type
@ -301,6 +300,7 @@ const useConnectionStore = defineStore('connections', {
} }
delete this.databases[name] delete this.databases[name]
delete this.serverStats[name]
const tabStore = useTabStore() const tabStore = useTabStore()
tabStore.removeTabByName(name) tabStore.removeTabByName(name)
@ -308,7 +308,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* close all connection * close all connections
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async closeAllConnection() { async closeAllConnection() {
@ -338,7 +338,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* create connection group * create a connection group
* @param name * @param name
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
*/ */
@ -441,14 +441,15 @@ const useConnectionStore = defineStore('connections', {
/** /**
* load redis key * load redis key
* @param server * @param {string} server
* @param db * @param {number} db
* @param key * @param {string} [key] when key is null or blank, update tab to display normal content (blank content or server status)
*/ */
async loadKeyValue(server, db, key) { async loadKeyValue(server, db, key) {
try { try {
const { data, success, msg } = await GetKeyValue(server, db, key)
const tab = useTabStore() const tab = useTabStore()
if (!isEmpty(key)) {
const { data, success, msg } = await GetKeyValue(server, db, key)
if (success) { if (success) {
const { type, ttl, value } = data const { type, ttl, value } = data
tab.upsertTab({ tab.upsertTab({
@ -459,16 +460,18 @@ const useConnectionStore = defineStore('connections', {
key, key,
value, value,
}) })
} else { return
}
}
tab.upsertTab({ tab.upsertTab({
server, server,
db, db,
type: 'none', type: 'none',
ttl: -1, ttl: -1,
key, key: null,
value: null, value: null,
}) })
}
} finally { } finally {
} }
}, },
@ -582,7 +585,6 @@ const useConnectionStore = defineStore('connections', {
selectedNode = { selectedNode = {
key: `${connName}/db${db}${nodeKey}`, key: `${connName}/db${db}${nodeKey}`,
label: keyPart[i], label: keyPart[i],
name: connName,
db, db,
keys: 0, keys: 0,
redisKey: handlePath, redisKey: handlePath,
@ -601,7 +603,6 @@ const useConnectionStore = defineStore('connections', {
const selectedNode = { const selectedNode = {
key: `${connName}/db${db}${nodeKey}`, key: `${connName}/db${db}${nodeKey}`,
label: keyPart[i], label: keyPart[i],
name: connName,
db, db,
keys: 0, keys: 0,
redisKey: handlePath, redisKey: handlePath,
@ -612,7 +613,6 @@ const useConnectionStore = defineStore('connections', {
children.push(selectedNode) children.push(selectedNode)
} }
} }
console.log('count:', ++count)
} }
}, },