From 5a86bab647e7ba1a9d56faf553cb35f1d798667d Mon Sep 17 00:00:00 2001 From: Lykin <137850705+tiny-craft@users.noreply.github.com> Date: Thu, 25 Jan 2024 00:29:15 +0800 Subject: [PATCH] feat: add connected client list --- backend/services/browser_service.go | 49 ++++++++ .../content_value/ContentServerStatus.vue | 105 +++++++++++++++++- .../content_value/ContentToolbar.vue | 10 +- frontend/src/langs/en-us.json | 9 +- frontend/src/langs/zh-cn.json | 9 +- frontend/src/stores/browser.js | 21 ++++ frontend/src/utils/date.js | 17 +++ 7 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 frontend/src/utils/date.js diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index 0de4e34..dfca769 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -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 +} diff --git a/frontend/src/components/content_value/ContentServerStatus.vue b/frontend/src/components/content_value/ContentServerStatus.vue index 1f6ccb9..a402b4b 100644 --- a/frontend/src/components/content_value/ContentServerStatus.vue +++ b/frontend/src/components/content_value/ContentServerStatus.vue @@ -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) + }, + }, + ], + }, + ] +})