feat: add connected client list
This commit is contained in:
parent
fe2f8a0480
commit
5a86bab647
|
@ -2513,3 +2513,52 @@ func (b *browserService) GetSlowLogs(server string, num int64) (resp types.JSRes
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetClientList get all connected client info
|
||||
func (b *browserService) GetClientList(server string) (resp types.JSResp) {
|
||||
item, err := b.getRedisClient(server, -1)
|
||||
if err != nil {
|
||||
resp.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
parseContent := func(content string) []map[string]string {
|
||||
lines := strings.Split(content, "\n")
|
||||
list := make([]map[string]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) > 0 {
|
||||
items := strings.Split(line, " ")
|
||||
itemKV := map[string]string{}
|
||||
for _, it := range items {
|
||||
kv := strings.SplitN(it, "=", 2)
|
||||
if len(kv) > 1 {
|
||||
itemKV[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
list = append(list, itemKV)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
client, ctx := item.client, item.ctx
|
||||
var fullList []map[string]string
|
||||
var mutex sync.Mutex
|
||||
if cluster, ok := client.(*redis.ClusterClient); ok {
|
||||
cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
fullList = append(fullList, parseContent(cli.ClientList(ctx).Val())...)
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
fullList = append(fullList, parseContent(client.ClientList(ctx).Val())...)
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = map[string]any{
|
||||
"list": fullList,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -14,20 +14,21 @@ import {
|
|||
toArray,
|
||||
toNumber,
|
||||
} from 'lodash'
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from 'vue'
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from 'vue'
|
||||
import IconButton from '@/components/common/IconButton.vue'
|
||||
import Filter from '@/components/icons/Filter.vue'
|
||||
import Refresh from '@/components/icons/Refresh.vue'
|
||||
import useBrowserStore from 'stores/browser.js'
|
||||
import { timeout } from '@/utils/promise.js'
|
||||
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
||||
import { NIcon, useThemeVars } from 'naive-ui'
|
||||
import { NButton, NIcon, NSpace, useThemeVars } from 'naive-ui'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { convertBytes, formatBytes } from '@/utils/byte_convert.js'
|
||||
import usePreferencesStore from 'stores/preferences.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useConnectionStore from 'stores/connections.js'
|
||||
import { toHumanReadable } from '@/utils/date.js'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -501,6 +502,87 @@ const byteChartOption = computed(() => {
|
|||
},
|
||||
}
|
||||
})
|
||||
|
||||
const clientInfo = reactive({
|
||||
loading: false,
|
||||
content: [],
|
||||
})
|
||||
const onShowClients = async (show) => {
|
||||
if (show) {
|
||||
try {
|
||||
clientInfo.loading = true
|
||||
clientInfo.content = await browserStore.getClientList(props.server)
|
||||
} finally {
|
||||
clientInfo.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clientTableColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'title',
|
||||
title: () => {
|
||||
return h(NSpace, { wrap: false, wrapItem: false, justify: 'center' }, () => [
|
||||
h('span', { style: { fontWeight: '550', fontSize: '15px' } }, i18n.t('status.client.title')),
|
||||
h(IconButton, {
|
||||
icon: Refresh,
|
||||
size: 16,
|
||||
onClick: () => onShowClients(true),
|
||||
}),
|
||||
])
|
||||
},
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
children: [
|
||||
{
|
||||
key: 'no',
|
||||
title: '#',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: (row, index) => {
|
||||
return index + 1
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'addr',
|
||||
title: () => i18n.t('status.client.addr'),
|
||||
sorter: 'default',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
},
|
||||
{
|
||||
key: 'db',
|
||||
title: () => i18n.t('status.client.db'),
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
},
|
||||
{
|
||||
key: 'age',
|
||||
title: () => i18n.t('status.client.age'),
|
||||
sorter: (row1, row2) => row1.age - row2.age,
|
||||
defaultSortOrder: 'descend',
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: ({ age }, index) => {
|
||||
return toHumanReadable(age)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'idle',
|
||||
title: () => i18n.t('status.client.idle'),
|
||||
sorter: (row1, row2) => row1.age - row2.age,
|
||||
align: 'center',
|
||||
titleAlign: 'center',
|
||||
render: ({ idle }, index) => {
|
||||
return toHumanReadable(idle)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -568,7 +650,24 @@ const byteChartOption = computed(() => {
|
|||
<n-gi :span="6">
|
||||
<n-statistic
|
||||
:label="$t('status.connected_clients')"
|
||||
:value="get(serverInfo, 'Clients.connected_clients', '0')" />
|
||||
:value="get(serverInfo, 'Clients.connected_clients', '0')">
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="click" width="70vw" @update-show="onShowClients">
|
||||
<template #trigger>
|
||||
<n-button :bordered="false" size="small">↘</n-button>
|
||||
</template>
|
||||
<n-data-table
|
||||
:columns="clientTableColumns"
|
||||
:data="clientInfo.content"
|
||||
:loading="clientInfo.loading"
|
||||
:single-column="false"
|
||||
:single-line="false"
|
||||
max-height="50vh"
|
||||
size="small"
|
||||
striped />
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-statistic :value="totalKeys">
|
||||
|
|
|
@ -14,7 +14,7 @@ import { computed, onMounted, onUnmounted, reactive, watch } from 'vue'
|
|||
import { NIcon, useThemeVars } from 'naive-ui'
|
||||
import { timeout } from '@/utils/promise.js'
|
||||
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { toHumanReadable } from '@/utils/date.js'
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
|
@ -61,13 +61,7 @@ const binaryKey = computed(() => {
|
|||
|
||||
const ttlString = computed(() => {
|
||||
if (ttl.value > 0) {
|
||||
const dur = dayjs.duration(ttl.value, 'seconds')
|
||||
const days = dur.days()
|
||||
if (days > 0) {
|
||||
return days + i18n.t('common.unit_day') + ' ' + dur.format('HH:mm:ss')
|
||||
} else {
|
||||
return dur.format('HH:mm:ss')
|
||||
}
|
||||
return toHumanReadable(ttl.value)
|
||||
} else if (ttl.value < 0) {
|
||||
return i18n.t('interface.forever')
|
||||
} else {
|
||||
|
|
|
@ -379,7 +379,14 @@
|
|||
"activity_status": "Activity",
|
||||
"act_cmd": "Commands Per Second",
|
||||
"act_network_input": "Network Input",
|
||||
"act_network_output": "Network Output"
|
||||
"act_network_output": "Network Output",
|
||||
"client": {
|
||||
"title": "Connected Client List",
|
||||
"addr": "Client Address",
|
||||
"age": "Age (sec)",
|
||||
"idle": "Idle (sec)",
|
||||
"db": "Database"
|
||||
}
|
||||
},
|
||||
"slog": {
|
||||
"title": "Slow Log",
|
||||
|
|
|
@ -379,7 +379,14 @@
|
|||
"activity_status": "活动状态",
|
||||
"act_cmd": "命令执行数/秒",
|
||||
"act_network_input": "网络输入",
|
||||
"act_network_output": "网络输出"
|
||||
"act_network_output": "网络输出",
|
||||
"client": {
|
||||
"title": "所有客户端列表",
|
||||
"addr": "客户端地址",
|
||||
"age": "连接时长(秒)",
|
||||
"idle": "空闲时长(秒)",
|
||||
"db": "数据库"
|
||||
}
|
||||
},
|
||||
"slog": {
|
||||
"title": "慢日志",
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
DeleteKeys,
|
||||
ExportKey,
|
||||
FlushDB,
|
||||
GetClientList,
|
||||
GetCmdHistory,
|
||||
GetKeyDetail,
|
||||
GetKeySummary,
|
||||
|
@ -1869,6 +1870,26 @@ const useBrowserStore = defineStore('browser', {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* get client list info
|
||||
* @param {string} server
|
||||
* @return {Promise<{idle: number, name: string, addr: string, age: number, db: number}[]>}
|
||||
*/
|
||||
async getClientList(server) {
|
||||
const { success, msg, data } = await GetClientList(server)
|
||||
if (success) {
|
||||
const { list = [] } = data
|
||||
return map(list, (item) => ({
|
||||
addr: item['addr'],
|
||||
name: item['name'],
|
||||
age: item['age'] || 0,
|
||||
idle: item['idle'] || 0,
|
||||
db: item['db'] || 0,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
/**
|
||||
* get slow log list
|
||||
* @param {string} server
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import dayjs from 'dayjs'
|
||||
import { i18nGlobal } from '@/utils/i18n.js'
|
||||
|
||||
/**
|
||||
* convert seconds number to human-readable string
|
||||
* @param {number} duration duration in seconds
|
||||
* @return {string}
|
||||
*/
|
||||
export const toHumanReadable = (duration) => {
|
||||
const dur = dayjs.duration(duration, 'seconds')
|
||||
const days = dur.days()
|
||||
if (days > 0) {
|
||||
return days + i18nGlobal.t('common.unit_day') + ' ' + dur.format('HH:mm:ss')
|
||||
} else {
|
||||
return dur.format('HH:mm:ss')
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue