feat: add server status page

This commit is contained in:
tiny-craft 2023-07-14 02:03:45 +08:00
parent 29f87f75c1
commit ddf69a3bb2
9 changed files with 226 additions and 45 deletions

View File

@ -184,7 +184,7 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
// get database info
res, err := rdb.Info(ctx, "keyspace").Result()
if err != nil {
resp.Msg = "list database fail:" + err.Error()
resp.Msg = "get server info fail:" + err.Error()
return
}
// Parse all db, response content like below
@ -304,6 +304,26 @@ func (c *connectionService) parseDBItemInfo(info string) map[string]int {
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
// @param path contain connection name and db name
func (c *connectionService) OpenDatabase(connName string, db int) (resp types.JSResp) {

View File

@ -1,14 +1,45 @@
<script setup>
import useDialog from '../../stores/dialog.js'
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 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>
<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 -->
<n-empty :description="$t('empty_server_content')">
<n-empty v-if="!hasContent" :description="$t('empty_server_content')">
<template #extra>
<n-button @click="dialogStore.openNewDialog()">
<template #icon>
@ -18,6 +49,12 @@ const dialogStore = useDialog()
</n-button>
</template>
</n-empty>
<content-server-status
v-else
v-model:auto-refresh="autoRefresh"
:server="connectionStore.selectedServer"
:info="serverInfo"
/>
</div>
</template>
@ -25,7 +62,9 @@ const dialogStore = useDialog()
@import 'content';
.content-container {
justify-content: center;
//justify-content: center;
padding: 5px;
box-sizing: border-box;
}
.color-preset-item {

View File

@ -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>

View File

@ -6,10 +6,6 @@ const props = defineProps({
type: Boolean,
default: false,
},
clickToggle: {
type: Boolean,
default: false,
},
strokeWidth: {
type: [Number, String],
default: 3,
@ -19,12 +15,6 @@ const props = defineProps({
default: '#dc423c',
},
})
const onToggle = () => {
if (props.clickToggle) {
emit('update:modelValue', !props.modelValue)
}
}
</script>
<template>

View File

@ -6,10 +6,6 @@ const props = defineProps({
type: Boolean,
default: false,
},
clickToggle: {
type: Boolean,
default: false,
},
strokeWidth: {
type: [Number, String],
default: 3,
@ -19,23 +15,10 @@ const props = defineProps({
default: '#dc423c',
},
})
const onToggle = () => {
if (props.clickToggle) {
emit('update:modelValue', !props.modelValue)
}
}
</script>
<template>
<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"
>
<svg v-if="props.modelValue" fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke="props.fillColor"
:stroke-width="props.strokeWidth"
@ -74,14 +57,7 @@ const onToggle = () => {
stroke-linejoin="round"
/>
</svg>
<svg
v-else
:height="props.size"
:width="props.size"
fill="none"
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"
>
<svg v-else fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
: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"

View File

@ -1,12 +1,12 @@
<script setup>
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 { NIcon, useDialog, useMessage } from 'naive-ui'
import { ConnectionType } from '../../consts/connection_type.js'
import ToggleFolder from '../icons/ToggleFolder.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 Delete from '../icons/Delete.vue'
import Unlink from '../icons/Unlink.vue'
@ -27,6 +27,18 @@ const message = useMessage()
const expandedKeys = 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({
filterPattern: {
type: String,

View File

@ -133,5 +133,13 @@
"log": "Log",
"about": "About",
"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"
}

View File

@ -136,5 +136,13 @@
"log": "日志",
"about": "关于",
"help": "帮助",
"check_update": "检查更新..."
"check_update": "检查更新...",
"auto_refresh": "自动刷新",
"uptime": "运行时间",
"connected_clients": "已连客户端",
"total_keys": "键总数",
"memory_used": "内存使用",
"unit_day": "天",
"unit_hour": "小时",
"unit_minute": "分钟"
}

View File

@ -19,6 +19,7 @@ import {
SaveConnection,
SaveSortedConnection,
ScanKeys,
ServerInfo,
SetHashValue,
SetKeyTTL,
SetKeyValue,
@ -58,8 +59,8 @@ const useConnectionStore = defineStore('connections', {
/**
* @typedef {Object} ConnectionState
* @property {string[]} groups
* @property {Object.<string, DatabaseItem[]>} databases
* @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'
*/
@ -70,6 +71,8 @@ const useConnectionStore = defineStore('connections', {
state: () => ({
groups: [], // all group name set
connections: [], // all connections
selectedServer: '', // current selected server
serverStats: {}, // current server status info
databases: {}, // all databases in opened connections group by server name
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}`]
},
/**
*
* @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
* @param server