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 },