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
|
||||
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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": "分钟"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue