diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go
index 24d0d97..217d728 100644
--- a/backend/services/connection_service.go
+++ b/backend/services/connection_service.go
@@ -74,7 +74,7 @@ func (c *connectionService) Stop(ctx context.Context) {
c.connMap = map[string]connectionItem{}
}
-func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*redis.Client, error) {
+func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.Options, error) {
var sshClient *ssh.Client
if config.SSH.Enable {
sshConfig := &ssh.ClientConfig{
@@ -127,10 +127,21 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*r
option.ReadTimeout = -2
option.WriteTimeout = -2
}
+ return option, nil
+}
+
+func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*redis.Client, error) {
+ option, err := c.buildOption(config)
+ if err != nil {
+ return nil, err
+ }
if config.Sentinel.Enable {
sentinel := redis.NewSentinelClient(option)
- addr, err := sentinel.GetMasterAddrByName(c.ctx, config.Sentinel.Master).Result()
+ defer sentinel.Close()
+
+ var addr []string
+ addr, err = sentinel.GetMasterAddrByName(c.ctx, config.Sentinel.Master).Result()
if err != nil {
return nil, err
}
@@ -146,6 +157,40 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*r
return rdb, nil
}
+// ListSentinelMasters list all master info by sentinel
+func (c *connectionService) ListSentinelMasters(config types.ConnectionConfig) (resp types.JSResp) {
+ option, err := c.buildOption(config)
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+
+ if option.DialTimeout > 0 {
+ option.DialTimeout = 10 * time.Second
+ }
+ sentinel := redis.NewSentinelClient(option)
+ defer sentinel.Close()
+
+ var retInfo []map[string]string
+ masterInfos, err := sentinel.Masters(c.ctx).Result()
+ if err != nil {
+ resp.Msg = err.Error()
+ return
+ }
+ for _, info := range masterInfos {
+ if infoMap, ok := info.(map[any]any); ok {
+ retInfo = append(retInfo, map[string]string{
+ "name": infoMap["name"].(string),
+ "addr": fmt.Sprintf("%s:%s", infoMap["ip"].(string), infoMap["port"].(string)),
+ })
+ }
+ }
+
+ resp.Data = retInfo
+ resp.Success = true
+ return
+}
+
func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp types.JSResp) {
rdb, err := c.createRedisClient(config)
if err != nil {
@@ -153,7 +198,8 @@ func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp
return
}
defer rdb.Close()
- if _, err := rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
+
+ if _, err = rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
resp.Msg = err.Error()
} else {
resp.Success = true
diff --git a/backend/storage/connections.go b/backend/storage/connections.go
index d4ecdb8..e98cbf3 100644
--- a/backend/storage/connections.go
+++ b/backend/storage/connections.go
@@ -195,8 +195,7 @@ func (c *ConnectionsStorage) UpdateConnection(name string, param types.Connectio
updated = true
}
} else {
- err := retrieve(conn.Connections, name, param)
- if err != nil {
+ if err := retrieve(conn.Connections, name, param); err != nil {
return err
}
}
@@ -287,7 +286,7 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
return c.saveConnections(conns)
}
-// CreateGroup create new group
+// CreateGroup create a new group
func (c *ConnectionsStorage) CreateGroup(name string) error {
c.mutex.Lock()
defer c.mutex.Unlock()
@@ -333,7 +332,7 @@ func (c *ConnectionsStorage) RenameGroup(name, newName string) error {
return c.saveConnections(conns)
}
-// DeleteGroup remove special group, include all connections under it
+// DeleteGroup remove specified group, include all connections under it
func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error {
c.mutex.Lock()
defer c.mutex.Unlock()
diff --git a/backend/types/connection.go b/backend/types/connection.go
index 7c2708f..8333f0b 100644
--- a/backend/types/connection.go
+++ b/backend/types/connection.go
@@ -45,7 +45,7 @@ type ConnectionSSH struct {
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"`
LoginType string `json:"loginType" yaml:"login_type"`
- Username string `json:"username" yaml:"username"`
+ Username string `json:"username" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"`
Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"`
diff --git a/frontend/src/components/dialogs/ConnectionDialog.vue b/frontend/src/components/dialogs/ConnectionDialog.vue
index dbc67ba..7f4ccac 100644
--- a/frontend/src/components/dialogs/ConnectionDialog.vue
+++ b/frontend/src/components/dialogs/ConnectionDialog.vue
@@ -2,7 +2,7 @@
import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
-import { SelectKeyFile, TestConnection } from 'wailsjs/go/services/connectionService.js'
+import { ListSentinelMasters, SelectKeyFile, TestConnection } from 'wailsjs/go/services/connectionService.js'
import useDialog, { ConnDialogType } from 'stores/dialog'
import Close from '@/components/icons/Close.vue'
import useConnectionStore from 'stores/connections.js'
@@ -57,17 +57,27 @@ const groupOptions = computed(() => {
})
const dbFilterList = ref([])
-const onUpdateDBFilterList = (list) => {
- const dbList = []
- for (const item of list) {
- const idx = toNumber(item)
- if (!isNaN(idx)) {
- dbList.push(idx)
+const onUpdateDBFilterType = (t) => {
+ if (t !== 'none') {
+ // set default filter index if empty
+ if (isEmpty(dbFilterList.value)) {
+ dbFilterList.value = ['0']
}
}
- generalForm.value.dbFilterList = sortBy(dbList)
}
+watch(
+ () => dbFilterList.value,
+ (list) => {
+ const dbList = map(list, (item) => {
+ const idx = toNumber(item)
+ return isNaN(idx) ? 0 : idx
+ })
+ generalForm.value.dbFilterList = sortBy(dbList)
+ },
+ { deep: true },
+)
+
const sshLoginType = computed(() => {
return get(generalForm.value, 'ssh.loginType', 'pwd')
})
@@ -81,6 +91,36 @@ const onChoosePKFile = async () => {
}
}
+const loadingSentinelMaster = ref(false)
+const masterNameOptions = ref([])
+const onLoadSentinelMasters = async () => {
+ try {
+ loadingSentinelMaster.value = true
+ const { success, data, msg } = await ListSentinelMasters(generalForm.value)
+ if (!success || isEmpty(data)) {
+ $message.error(msg || 'list sentinel master fail')
+ } else {
+ const options = []
+ for (const m of data) {
+ options.push({
+ label: m['name'],
+ value: m['name'],
+ })
+ }
+
+ // select default names
+ if (!isEmpty(options)) {
+ generalForm.value.sentinel.master = options[0].value
+ }
+ masterNameOptions.value = options
+ }
+ } catch (e) {
+ $message.error(e.message)
+ } finally {
+ loadingSentinelMaster.value = false
+ }
+}
+
const tab = ref('general')
const testing = ref(false)
const showTestResult = ref(false)
@@ -104,6 +144,11 @@ const onSaveConnection = async () => {
}
})
+ // trim advance data
+ if (get(generalForm.value, 'dbFilterType', 'none') === 'none') {
+ generalForm.value.dbFilterList = []
+ }
+
// trim ssh login data
if (generalForm.value.ssh.enable) {
switch (generalForm.value.ssh.loginType) {
@@ -121,6 +166,13 @@ const onSaveConnection = async () => {
generalForm.value.ssh = ssh
}
+ // trim sentinel data
+ if (!generalForm.value.sentinel.enable) {
+ generalForm.value.sentinel.master = ''
+ generalForm.value.sentinel.username = ''
+ generalForm.value.sentinel.password = ''
+ }
+
// store new connection
const { success, msg } = await connectionStore.saveConnection(
isEditMode.value ? editName.value : null,
@@ -142,6 +194,7 @@ const resetForm = () => {
showTestResult.value = false
testResult.value = ''
tab.value = 'general'
+ loadingSentinelMaster.value = false
}
watch(
@@ -286,7 +339,9 @@ const onClose = () => {
-
+
{
value="hide" />
-
+
{
:placeholder="$t('dialogue.connection.advn.dbfilter_input_tip')"
:show-arrow="false"
:show="false"
- :clearable="true"
- @update:value="onUpdateDBFilterList" />
+ :clearable="true" />
{
:disabled="!generalForm.sentinel.enable"
label-placement="top">
-
+
+
+
+ {{ $t('dialogue.connection.sentinel.auto_discover') }}
+
+
{
-
diff --git a/frontend/src/langs/en.json b/frontend/src/langs/en.json
index 918eeb7..342927b 100644
--- a/frontend/src/langs/en.json
+++ b/frontend/src/langs/en.json
@@ -122,8 +122,8 @@
"pwd": "Password",
"name_tip": "Connection name",
"addr_tip": "Redis server host",
- "usr_tip": "(Optional) Redis server username",
- "pwd_tip": "(Optional) Redis server authentication password (Redis > 6.0)",
+ "usr_tip": "(Optional) Authentication username",
+ "pwd_tip": "(Optional) Authentication password (Redis > 6.0)",
"test": "Test Connection",
"test_succ": "Successful connection to redis-server",
"test_fail": "Fail Connection",
@@ -161,11 +161,12 @@
"sentinel": {
"title": "Sentinel",
"enable": "Serve as Sentinel Node",
- "master": "Name of Master Node",
+ "master": "Master Group Name",
+ "auto_discover": "Auto Discover",
"password": "Password for Master Node",
"username": "Username for Master Node",
- "pwd_tip": "(Optional) username for master node",
- "usr_tip": "(Optional) authentication password for master node (Redis > 6.0)"
+ "pwd_tip": "(Optional) Authentication username for master node",
+ "usr_tip": "(Optional) Authentication password for master node (Redis > 6.0)"
}
},
"group": {
diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json
index 8368242..243db65 100644
--- a/frontend/src/langs/zh-cn.json
+++ b/frontend/src/langs/zh-cn.json
@@ -132,7 +132,7 @@
"filter": "默认键过滤表达式",
"filter_tip": "需要加载的键名表达式",
"separator": "键分隔符",
- "separator_tip":"键名路径分隔符",
+ "separator_tip": "键名路径分隔符",
"conn_timeout": "连接超时",
"exec_timeout": "执行超时",
"dbfilter_type": "数据库过滤方式",
@@ -161,7 +161,8 @@
"sentinel": {
"title": "哨兵模式",
"enable": "当前为哨兵节点",
- "master": "主实例名",
+ "master": "主节点组名",
+ "auto_discover": "自动查询组名",
"password": "主节点密码",
"username": "主节点用户名",
"pwd_tip": "(可选)主节点服务授权用户名",
@@ -209,7 +210,7 @@
"ttl": {
"title": "设置键存活时间"
},
- "upgrade":{
+ "upgrade": {
"title": "有可用新版本",
"new_version_tip": "新版本({ver}),是否立即下载",
"no_update": "当前已是最新版",
diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js
index a998366..b478113 100644
--- a/frontend/src/stores/connections.js
+++ b/frontend/src/stores/connections.js
@@ -230,6 +230,25 @@ const useConnectionStore = defineStore('connections', {
}
},
+ mergeConnectionProfile(dest, src) {
+ const mergeObj = (destObj, srcObj) => {
+ for (const k in srcObj) {
+ const t = typeof srcObj[k]
+ if (t === 'string') {
+ destObj[k] = srcObj[k] || destObj[k] || ''
+ } else if (t === 'number') {
+ destObj[k] = srcObj[k] || destObj[k] || 0
+ } else if (t === 'object') {
+ mergeObj(destObj[k], srcObj[k] || {})
+ } else {
+ destObj[k] = srcObj[k]
+ }
+ }
+ return destObj
+ }
+ return mergeObj(dest, src)
+ },
+
/**
* get database server by name
* @param name
diff --git a/frontend/src/stores/dialog.js b/frontend/src/stores/dialog.js
index bcbaa80..7d5d4b4 100644
--- a/frontend/src/stores/dialog.js
+++ b/frontend/src/stores/dialog.js
@@ -82,16 +82,7 @@ const useDialogStore = defineStore('dialog', {
async openEditDialog(name) {
const connStore = useConnectionStore()
const profile = await connStore.getConnectionProfile(name)
- const assignCustomizer = (objVal, srcVal, key) => {
- if (isEmpty(objVal)) {
- return srcVal
- }
- if (isEmpty(srcVal)) {
- return objVal
- }
- return undefined
- }
- this.connParam = assignWith({}, connStore.newDefaultConnection(name), profile, assignCustomizer)
+ this.connParam = connStore.mergeConnectionProfile(connStore.newDefaultConnection(name), profile)
this.connType = ConnDialogType.EDIT
this.connDialogVisible = true
},