Compare commits

..

No commits in common. "5a86bab647e7ba1a9d56faf553cb35f1d798667d" and "edaef2a78cba77307a132ae7ed98ec836c15596c" have entirely different histories.

11 changed files with 80 additions and 322 deletions

View File

@ -2513,52 +2513,3 @@ func (b *browserService) GetSlowLogs(server string, num int64) (resp types.JSRes
} }
return 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
}

View File

@ -14,21 +14,20 @@ import {
toArray, toArray,
toNumber, toNumber,
} from 'lodash' } from 'lodash'
import { computed, h, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from 'vue' import { computed, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from 'vue'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import Filter from '@/components/icons/Filter.vue' import Filter from '@/components/icons/Filter.vue'
import Refresh from '@/components/icons/Refresh.vue' import Refresh from '@/components/icons/Refresh.vue'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { timeout } from '@/utils/promise.js' import { timeout } from '@/utils/promise.js'
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue' import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
import { NButton, NIcon, NSpace, useThemeVars } from 'naive-ui' import { NIcon, useThemeVars } from 'naive-ui'
import { Line } from 'vue-chartjs' import { Line } from 'vue-chartjs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { convertBytes, formatBytes } from '@/utils/byte_convert.js' import { convertBytes, formatBytes } from '@/utils/byte_convert.js'
import usePreferencesStore from 'stores/preferences.js' import usePreferencesStore from 'stores/preferences.js'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import { toHumanReadable } from '@/utils/date.js'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -502,87 +501,6 @@ 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> </script>
<template> <template>
@ -650,24 +568,7 @@ const clientTableColumns = computed(() => {
<n-gi :span="6"> <n-gi :span="6">
<n-statistic <n-statistic
:label="$t('status.connected_clients')" :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">&LowerRightArrow;</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>
<n-gi :span="6"> <n-gi :span="6">
<n-statistic :value="totalKeys"> <n-statistic :value="totalKeys">

View File

@ -10,11 +10,11 @@ import { useI18n } from 'vue-i18n'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import Copy from '@/components/icons/Copy.vue' import Copy from '@/components/icons/Copy.vue'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js' import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { computed, onMounted, onUnmounted, reactive, watch } from 'vue' import { computed, onUnmounted, reactive, watch } from 'vue'
import { NIcon, useThemeVars } from 'naive-ui' import { NIcon, useThemeVars } from 'naive-ui'
import { timeout } from '@/utils/promise.js' import { timeout } from '@/utils/promise.js'
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue' import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
import { toHumanReadable } from '@/utils/date.js' import dayjs from 'dayjs'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -45,12 +45,6 @@ const autoRefresh = reactive({
interval: 2, interval: 2,
}) })
const ttl = reactive({
value: 0,
expire: 0,
intervalID: 0,
})
const themeVars = useThemeVars() const themeVars = useThemeVars()
const dialogStore = useDialog() const dialogStore = useDialog()
const i18n = useI18n() const i18n = useI18n()
@ -60,9 +54,15 @@ const binaryKey = computed(() => {
}) })
const ttlString = computed(() => { const ttlString = computed(() => {
if (ttl.value > 0) { if (props.ttl > 0) {
return toHumanReadable(ttl.value) const dur = dayjs.duration(props.ttl, 'seconds')
} else if (ttl.value < 0) { 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')
}
} else if (props.ttl < 0) {
return i18n.t('interface.forever') return i18n.t('interface.forever')
} else { } else {
return '00:00:00' return '00:00:00'
@ -89,46 +89,15 @@ const stopAutoRefresh = () => {
autoRefresh.on = false autoRefresh.on = false
} }
const syncTTL = (seconds) => {
ttl.value = seconds
if (seconds >= 0) {
ttl.expire = Math.floor(Date.now() / 1000 + seconds)
} else {
ttl.expire = 0
}
}
watch( watch(
() => props.keyPath, () => props.keyPath,
() => { () => {
stopAutoRefresh() stopAutoRefresh()
autoRefresh.interval = props.interval
}, },
) )
watch( onUnmounted(() => stopAutoRefresh())
() => props.ttl,
(seconds) => syncTTL(seconds),
)
onMounted(() => {
syncTTL(props.ttl)
ttl.intervalID = setInterval(() => {
if (ttl.expire > 0) {
const nowSeconds = Math.floor(Date.now() / 1000)
ttl.value = Math.max(0, ttl.expire - nowSeconds)
} else {
ttl.value = -1
}
}, 1000)
})
onUnmounted(() => {
stopAutoRefresh()
if (ttl.intervalID > 0) {
clearInterval(ttl.intervalID)
ttl.intervalID = 0
}
})
const onToggleRefresh = (on) => { const onToggleRefresh = (on) => {
if (on) { if (on) {
@ -199,7 +168,7 @@ const onTTL = () => {
<template #icon> <template #icon>
<n-icon :component="Timer" size="18" /> <n-icon :component="Timer" size="18" />
</template> </template>
<span style="font-variant-numeric: tabular-nums">{{ ttlString }}</span> {{ ttlString }}
</n-button> </n-button>
</template> </template>
TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }} TTL{{ `${ttl > 0 ? ': ' + ttl + $t('common.second') : ''}` }}

View File

@ -567,11 +567,7 @@ defineExpose({
</script> </script>
<template> <template>
<div <div :style="{ backgroundColor }" class="flex-box-v browser-tree-wrapper" @contextmenu="(e) => e.preventDefault()">
:style="{ backgroundColor }"
class="flex-box-v browser-tree-wrapper"
@contextmenu="(e) => e.preventDefault()"
@keydown.esc="contextMenuParam.show = false">
<n-spin v-if="props.loading" class="fill-height" /> <n-spin v-if="props.loading" class="fill-height" />
<n-empty v-else-if="!props.loading && isEmpty(data)" class="empty-content" /> <n-empty v-else-if="!props.loading && isEmpty(data)" class="empty-content" />
<n-tree <n-tree

View File

@ -479,7 +479,6 @@ const onCancelOpen = () => {
</script> </script>
<template> <template>
<div class="connection-tree-wrapper" @keydown.esc="contextMenuParam.show = false">
<n-empty <n-empty
v-if="isEmpty(connectionStore.connections)" v-if="isEmpty(connectionStore.connections)"
:description="$t('interface.empty_server_list')" :description="$t('interface.empty_server_list')"
@ -539,14 +538,8 @@ const onCancelOpen = () => {
trigger="manual" trigger="manual"
@clickoutside="contextMenuParam.show = false" @clickoutside="contextMenuParam.show = false"
@select="handleSelectContextMenu" /> @select="handleSelectContextMenu" />
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/content'; @import '@/styles/content';
.connection-tree-wrapper {
height: 100%;
overflow: hidden;
}
</style> </style>

View File

@ -379,14 +379,7 @@
"activity_status": "Activity", "activity_status": "Activity",
"act_cmd": "Commands Per Second", "act_cmd": "Commands Per Second",
"act_network_input": "Network Input", "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": { "slog": {
"title": "Slow Log", "title": "Slow Log",

View File

@ -379,14 +379,7 @@
"activity_status": "活动状态", "activity_status": "活动状态",
"act_cmd": "命令执行数/秒", "act_cmd": "命令执行数/秒",
"act_network_input": "网络输入", "act_network_input": "网络输入",
"act_network_output": "网络输出", "act_network_output": "网络输出"
"client": {
"title": "所有客户端列表",
"addr": "客户端地址",
"age": "连接时长(秒)",
"idle": "空闲时长(秒)",
"db": "数据库"
}
}, },
"slog": { "slog": {
"title": "慢日志", "title": "慢日志",

View File

@ -13,7 +13,6 @@ import {
DeleteKeys, DeleteKeys,
ExportKey, ExportKey,
FlushDB, FlushDB,
GetClientList,
GetCmdHistory, GetCmdHistory,
GetKeyDetail, GetKeyDetail,
GetKeySummary, GetKeySummary,
@ -1870,26 +1869,6 @@ 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 * get slow log list
* @param {string} server * @param {string} server

View File

@ -1,17 +0,0 @@
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')
}
}

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.21
require ( require (
github.com/adrg/sysfont v0.1.2 github.com/adrg/sysfont v0.1.2
github.com/andybalholm/brotli v1.1.0 github.com/andybalholm/brotli v1.1.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.5.0
github.com/klauspost/compress v1.17.4 github.com/klauspost/compress v1.17.4
github.com/redis/go-redis/v9 v9.4.0 github.com/redis/go-redis/v9 v9.4.0
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68

4
go.sum
View File

@ -26,8 +26,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=