feat: add server status page
This commit is contained in:
parent
29f87f75c1
commit
ddf69a3bb2
|
@ -184,7 +184,7 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
|
||||||
// get database info
|
// get database info
|
||||||
res, err := rdb.Info(ctx, "keyspace").Result()
|
res, err := rdb.Info(ctx, "keyspace").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Msg = "list database fail:" + err.Error()
|
resp.Msg = "get server info fail:" + err.Error()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Parse all db, response content like below
|
// Parse all db, response content like below
|
||||||
|
@ -304,6 +304,26 @@ func (c *connectionService) parseDBItemInfo(info string) map[string]int {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServerInfo get server info
|
||||||
|
func (c *connectionService) ServerInfo(name string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(name, 0)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get database info
|
||||||
|
res, err := rdb.Info(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = "get server info fail:" + err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = c.parseInfo(res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 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) (resp types.JSResp) {
|
func (c *connectionService) OpenDatabase(connName string, db int) (resp types.JSResp) {
|
||||||
|
|
|
@ -1,14 +1,45 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import useDialog from '../../stores/dialog.js'
|
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 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">
|
<div class="content-container flex-box-v" :style="{ 'justify-content': hasContent ? 'flex-start' : 'center' }">
|
||||||
<!-- TODO: replace icon to app icon -->
|
<!-- TODO: replace icon to app icon -->
|
||||||
<n-empty :description="$t('empty_server_content')">
|
<n-empty v-if="!hasContent" :description="$t('empty_server_content')">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<n-button @click="dialogStore.openNewDialog()">
|
<n-button @click="dialogStore.openNewDialog()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -18,6 +49,12 @@ const dialogStore = useDialog()
|
||||||
</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>
|
||||||
|
|
||||||
|
@ -25,7 +62,9 @@ const dialogStore = useDialog()
|
||||||
@import 'content';
|
@import 'content';
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
justify-content: center;
|
//justify-content: center;
|
||||||
|
padding: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-preset-item {
|
.color-preset-item {
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script setup>
|
||||||
|
import { get, mapValues, pickBy, split, sum, toArray, toNumber } from 'lodash'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import Help from '../icons/Help.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
server: String,
|
||||||
|
info: Object,
|
||||||
|
autoRefresh: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:autoRefresh'])
|
||||||
|
|
||||||
|
const redisVersion = computed(() => {
|
||||||
|
return get(props.info, 'redis_version', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
const redisMode = computed(() => {
|
||||||
|
return get(props.info, 'redis_mode', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
const role = computed(() => {
|
||||||
|
return get(props.info, 'role', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeUnit = ['unit_minute', 'unit_hour', 'unit_day']
|
||||||
|
const uptime = computed(() => {
|
||||||
|
let seconds = get(props.info, 'uptime_in_seconds', 0)
|
||||||
|
seconds /= 60
|
||||||
|
if (seconds < 60) {
|
||||||
|
// minutes
|
||||||
|
return [Math.floor(seconds), timeUnit[0]]
|
||||||
|
}
|
||||||
|
seconds /= 60
|
||||||
|
if (seconds < 60) {
|
||||||
|
// hours
|
||||||
|
return [Math.floor(seconds), timeUnit[1]]
|
||||||
|
}
|
||||||
|
return [Math.floor(seconds / 24), timeUnit[2]]
|
||||||
|
})
|
||||||
|
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const usedMemory = computed(() => {
|
||||||
|
let size = get(props.info, 'used_memory', 0)
|
||||||
|
let unitIndex = 0
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024
|
||||||
|
unitIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
return [size.toFixed(2), units[unitIndex]]
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalKeys = computed(() => {
|
||||||
|
const regex = /^db\d+$/
|
||||||
|
const result = pickBy(props.info, (value, key) => {
|
||||||
|
return regex.test(key)
|
||||||
|
})
|
||||||
|
const nums = mapValues(result, (v) => {
|
||||||
|
const keys = split(v, ',', 1)[0]
|
||||||
|
const num = split(keys, '=', 2)[1]
|
||||||
|
return toNumber(num)
|
||||||
|
})
|
||||||
|
return sum(toArray(nums))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card :theme-override1s="{ paddingMedium: '10px 20px 10px' }">
|
||||||
|
<template #header>
|
||||||
|
{{ props.server }}
|
||||||
|
<n-space inline size="small">
|
||||||
|
<n-tag v-if="redisVersion" type="primary" size="small">v{{ redisVersion }}</n-tag>
|
||||||
|
<n-tag v-if="redisMode" type="primary" size="small">{{ redisMode }}</n-tag>
|
||||||
|
<n-tag v-if="role" type="primary" size="small">{{ role }}</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-space inline size="small">
|
||||||
|
{{ $t('auto_refresh') }}
|
||||||
|
<n-switch :value="props.autoRefresh" @update:value="(v) => emit('update:autoRefresh', v)" />
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<n-grid x-gap="5" style="min-width: 500px">
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic :label="$t('uptime')" :value="uptime[0]">
|
||||||
|
<template #suffix> {{ $t(uptime[1]) }}</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic :label="$t('connected_clients')" :value="get(props.info, 'connected_clients', 0)" />
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic :value="totalKeys">
|
||||||
|
<template #label>{{ $t('total_keys') }} <n-icon :component="Help" /></template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic :label="$t('memory_used')" :value="usedMemory[0]">
|
||||||
|
<template #suffix> {{ usedMemory[1] }}</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
|
@ -6,10 +6,6 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
clickToggle: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
strokeWidth: {
|
strokeWidth: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: 3,
|
default: 3,
|
||||||
|
@ -19,12 +15,6 @@ const props = defineProps({
|
||||||
default: '#dc423c',
|
default: '#dc423c',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onToggle = () => {
|
|
||||||
if (props.clickToggle) {
|
|
||||||
emit('update:modelValue', !props.modelValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -6,10 +6,6 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
clickToggle: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
strokeWidth: {
|
strokeWidth: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: 3,
|
default: 3,
|
||||||
|
@ -19,23 +15,10 @@ const props = defineProps({
|
||||||
default: '#dc423c',
|
default: '#dc423c',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onToggle = () => {
|
|
||||||
if (props.clickToggle) {
|
|
||||||
emit('update:modelValue', !props.modelValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg
|
<svg v-if="props.modelValue" fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
v-if="props.modelValue"
|
|
||||||
:height="props.size"
|
|
||||||
:width="props.size"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
:stroke="props.fillColor"
|
:stroke="props.fillColor"
|
||||||
:stroke-width="props.strokeWidth"
|
:stroke-width="props.strokeWidth"
|
||||||
|
@ -74,14 +57,7 @@ const onToggle = () => {
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg
|
<svg v-else fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
v-else
|
|
||||||
:height="props.size"
|
|
||||||
:width="props.size"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 48 48"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
:stroke-width="props.strokeWidth"
|
:stroke-width="props.strokeWidth"
|
||||||
d="M41 4H7C5.34315 4 4 5.34315 4 7V41C4 42.6569 5.34315 44 7 44H41C42.6569 44 44 42.6569 44 41V7C44 5.34315 42.6569 4 41 4Z"
|
d="M41 4H7C5.34315 4 4 5.34315 4 7V41C4 42.6569 5.34315 44 7 44H41C42.6569 44 44 42.6569 44 41V7C44 5.34315 42.6569 4 41 4Z"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import useDialogStore from '../../stores/dialog.js'
|
import useDialogStore from '../../stores/dialog.js'
|
||||||
import { h, nextTick, reactive, ref } from 'vue'
|
import { h, nextTick, reactive, ref, watch } from 'vue'
|
||||||
import useConnectionStore from '../../stores/connections.js'
|
import useConnectionStore from '../../stores/connections.js'
|
||||||
import { NIcon, useDialog, useMessage } from 'naive-ui'
|
import { NIcon, useDialog, useMessage } from 'naive-ui'
|
||||||
import { ConnectionType } from '../../consts/connection_type.js'
|
import { ConnectionType } from '../../consts/connection_type.js'
|
||||||
import ToggleFolder from '../icons/ToggleFolder.vue'
|
import ToggleFolder from '../icons/ToggleFolder.vue'
|
||||||
import ToggleServer from '../icons/ToggleServer.vue'
|
import ToggleServer from '../icons/ToggleServer.vue'
|
||||||
import { debounce, indexOf } from 'lodash'
|
import { debounce, indexOf, size, split } from 'lodash'
|
||||||
import Config from '../icons/Config.vue'
|
import Config from '../icons/Config.vue'
|
||||||
import Delete from '../icons/Delete.vue'
|
import Delete from '../icons/Delete.vue'
|
||||||
import Unlink from '../icons/Unlink.vue'
|
import Unlink from '../icons/Unlink.vue'
|
||||||
|
@ -27,6 +27,18 @@ const message = useMessage()
|
||||||
const expandedKeys = ref([])
|
const expandedKeys = ref([])
|
||||||
const selectedKeys = ref([])
|
const selectedKeys = ref([])
|
||||||
|
|
||||||
|
watch(selectedKeys, () => {
|
||||||
|
const key = selectedKeys.value[0]
|
||||||
|
// try 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,
|
||||||
|
|
|
@ -133,5 +133,13 @@
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"check_update": "Check for Updates..."
|
"check_update": "Check for Updates...",
|
||||||
|
"auto_refresh": "Auto Refresh",
|
||||||
|
"uptime": "Uptime",
|
||||||
|
"connected_clients": "Clients",
|
||||||
|
"total_keys": "Keys",
|
||||||
|
"memory_used": "Memory",
|
||||||
|
"unit_day": "D",
|
||||||
|
"unit_hour": "H",
|
||||||
|
"unit_minute": "M"
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,5 +136,13 @@
|
||||||
"log": "日志",
|
"log": "日志",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"check_update": "检查更新..."
|
"check_update": "检查更新...",
|
||||||
|
"auto_refresh": "自动刷新",
|
||||||
|
"uptime": "运行时间",
|
||||||
|
"connected_clients": "已连客户端",
|
||||||
|
"total_keys": "键总数",
|
||||||
|
"memory_used": "内存使用",
|
||||||
|
"unit_day": "天",
|
||||||
|
"unit_hour": "小时",
|
||||||
|
"unit_minute": "分钟"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
SaveConnection,
|
SaveConnection,
|
||||||
SaveSortedConnection,
|
SaveSortedConnection,
|
||||||
ScanKeys,
|
ScanKeys,
|
||||||
|
ServerInfo,
|
||||||
SetHashValue,
|
SetHashValue,
|
||||||
SetKeyTTL,
|
SetKeyTTL,
|
||||||
SetKeyValue,
|
SetKeyValue,
|
||||||
|
@ -58,8 +59,8 @@ const useConnectionStore = defineStore('connections', {
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ConnectionState
|
* @typedef {Object} ConnectionState
|
||||||
* @property {string[]} groups
|
* @property {string[]} groups
|
||||||
* @property {Object.<string, DatabaseItem[]>} databases
|
|
||||||
* @property {ConnectionItem[]} connections
|
* @property {ConnectionItem[]} connections
|
||||||
|
* @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'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -70,6 +71,8 @@ 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
|
||||||
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
|
||||||
}),
|
}),
|
||||||
|
@ -419,6 +422,23 @@ const useConnectionStore = defineStore('connections', {
|
||||||
delete this.nodeMap[`${connName}#${db}`]
|
delete this.nodeMap[`${connName}#${db}`]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param server
|
||||||
|
* @returns {Promise<{}>}
|
||||||
|
*/
|
||||||
|
async getServerInfo(server) {
|
||||||
|
try {
|
||||||
|
const { success, data } = await ServerInfo(server)
|
||||||
|
if (success) {
|
||||||
|
this.serverStats[server] = data
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load redis key
|
* load redis key
|
||||||
* @param server
|
* @param server
|
||||||
|
|
Loading…
Reference in New Issue