diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index cc698e8..86d6ad5 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -12,13 +12,22 @@ import ( . "tinyrdm/backend/storage" "tinyrdm/backend/types" maputil "tinyrdm/backend/utils/map" + mathutil "tinyrdm/backend/utils/math" redis2 "tinyrdm/backend/utils/redis" ) +type cmdHistoryItem struct { + timestamp int64 + Time string `json:"time"` + Server string `json:"server"` + Cmd string `json:"cmd"` +} + type connectionService struct { - ctx context.Context - conns *ConnectionsStorage - connMap map[string]connectionItem + ctx context.Context + conns *ConnectionsStorage + connMap map[string]connectionItem + cmdHistory []cmdHistoryItem } type connectionItem struct { @@ -230,7 +239,7 @@ func (c *connectionService) CloseConnection(name string) (resp types.JSResp) { } // get redis client from local cache or create a new open -// if db >= 0, also switch to db index +// if db >= 0, will also switch to db index func (c *connectionService) getRedisClient(connName string, db int) (*redis.Client, context.Context, error) { item, ok := c.connMap[connName] var rdb *redis.Client @@ -249,7 +258,19 @@ func (c *connectionService) getRedisClient(connName string, db int) (*redis.Clie Password: selConn.Password, ReadTimeout: -1, }) - rdb.AddHook(redis2.NewHook(connName)) + rdb.AddHook(redis2.NewHook(connName, func(cmd string) { + now := time.Now() + last := strings.LastIndex(cmd, ":") + if last != -1 { + cmd = cmd[:last] + } + c.cmdHistory = append(c.cmdHistory, cmdHistoryItem{ + timestamp: now.UnixMilli(), + Time: now.Format("2006-01-02 15:04:05"), + Server: connName, + Cmd: cmd, + }) + })) if _, err := rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil { return nil, nil, errors.New("can not connect to redis server:" + err.Error()) @@ -960,6 +981,28 @@ func (c *connectionService) RenameKey(connName string, db int, key, newKey strin return } +func (c *connectionService) GetCmdHistory(pageNo, pageSize int) (resp types.JSResp) { + resp.Success = true + if pageSize <= 0 || pageNo <= 0 { + // return all history + resp.Data = map[string]any{ + "list": c.cmdHistory, + "pageNo": 1, + "pageSize": -1, + } + } else { + total := len(c.cmdHistory) + startIndex := total / pageSize * (pageNo - 1) + endIndex := mathutil.Min(startIndex+pageSize, total) + resp.Data = map[string]any{ + "list": c.cmdHistory[startIndex:endIndex], + "pageNo": pageNo, + "pageSize": pageSize, + } + } + return +} + // update or insert key info to database //func (c *connectionService) updateDBKey(connName string, db int, keys []string, separator string) { // dbStruct := map[string]any{} diff --git a/backend/storage/connections.go b/backend/storage/connections.go index 17cf95e..7b31b1f 100644 --- a/backend/storage/connections.go +++ b/backend/storage/connections.go @@ -102,7 +102,7 @@ func (c *ConnectionsStorage) GetConnection(name string) *types.Connection { return findConn(name, "", conns) } -// GetGroup get connection group by name +// GetGroup get one connection group by name func (c *ConnectionsStorage) GetGroup(name string) *types.Connection { conns := c.getConnections() diff --git a/backend/utils/math/math_util.go b/backend/utils/math/math_util.go new file mode 100644 index 0000000..b250dcf --- /dev/null +++ b/backend/utils/math/math_util.go @@ -0,0 +1,93 @@ +package mathutil + +import ( + "math" + . "tinyrdm/backend/utils" +) + +// MaxWithIndex 查找所有元素中的最大值 +func MaxWithIndex[T Hashable](items ...T) (T, int) { + selIndex := -1 + for i, t := range items { + if selIndex < 0 { + selIndex = i + } else { + if t > items[selIndex] { + selIndex = i + } + } + } + return items[selIndex], selIndex +} + +func Max[T Hashable](items ...T) T { + val, _ := MaxWithIndex(items...) + return val +} + +// MinWithIndex 查找所有元素中的最小值 +func MinWithIndex[T Hashable](items ...T) (T, int) { + selIndex := -1 + for i, t := range items { + if selIndex < 0 { + selIndex = i + } else { + if t < items[selIndex] { + selIndex = i + } + } + } + return items[selIndex], selIndex +} + +func Min[T Hashable](items ...T) T { + val, _ := MinWithIndex(items...) + return val +} + +// Clamp 返回限制在minVal和maxVal范围内的value +func Clamp[T Hashable](value T, minVal T, maxVal T) T { + if minVal > maxVal { + minVal, maxVal = maxVal, minVal + } + if value < minVal { + value = minVal + } else if value > maxVal { + value = maxVal + } + return value +} + +// Abs 计算绝对值 +func Abs[T SignedNumber](val T) T { + return T(math.Abs(float64(val))) +} + +// Floor 向下取整 +func Floor[T SignedNumber | UnsignedNumber](val T) T { + return T(math.Floor(float64(val))) +} + +// Ceil 向上取整 +func Ceil[T SignedNumber | UnsignedNumber](val T) T { + return T(math.Ceil(float64(val))) +} + +// Round 四舍五入取整 +func Round[T SignedNumber | UnsignedNumber](val T) T { + return T(math.Round(float64(val))) +} + +// Sum 计算所有元素总和 +func Sum[T SignedNumber | UnsignedNumber](items ...T) T { + var sum T + for _, item := range items { + sum += item + } + return sum +} + +// Average 计算所有元素的平均值 +func Average[T SignedNumber | UnsignedNumber](items ...T) T { + return Sum(items...) / T(len(items)) +} diff --git a/backend/utils/redis/log_hook.go b/backend/utils/redis/log_hook.go index a599630..2bf096d 100644 --- a/backend/utils/redis/log_hook.go +++ b/backend/utils/redis/log_hook.go @@ -7,31 +7,41 @@ import ( "net" ) +type execCallback func(string) + type LogHook struct { - name string + name string + cmdExec execCallback } -func NewHook(name string) LogHook { - return LogHook{ - name: name, +func NewHook(name string, cmdExec execCallback) *LogHook { + return &LogHook{ + name: name, + cmdExec: cmdExec, } } -func (LogHook) DialHook(next redis.DialHook) redis.DialHook { +func (l *LogHook) DialHook(next redis.DialHook) redis.DialHook { return func(ctx context.Context, network, addr string) (net.Conn, error) { return next(ctx, network, addr) } } -func (LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { +func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { - log.Println(cmd.String()) + log.Println(cmd) + if l.cmdExec != nil { + l.cmdExec(cmd.String()) + } return next(ctx, cmd) } } -func (LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { +func (l *LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { for _, cmd := range cmds { - log.Println(cmd.String()) + log.Println("pipeline: ", cmd) + if l.cmdExec != nil { + l.cmdExec(cmd.String()) + } } return next(ctx, cmds) } diff --git a/frontend/src/AppContent.vue b/frontend/src/AppContent.vue index 0becb86..6400395 100644 --- a/frontend/src/AppContent.vue +++ b/frontend/src/AppContent.vue @@ -10,6 +10,7 @@ import ContentServerPane from './components/content/ContentServerPane.vue' import useTabStore from './stores/tab.js' import usePreferencesStore from './stores/preferences.js' import useConnectionStore from './stores/connections.js' +import ContentLogPane from './components/content/ContentLogPane.vue' const themeVars = useThemeVars() @@ -60,8 +61,8 @@ const dragging = computed(() => {