feat: add discovery master group name for sentinel mode config

perf: tidy connection profile file
This commit is contained in:
tiny-craft 2023-10-08 15:24:08 +08:00
parent ee68d699fa
commit b5dfe377fa
8 changed files with 162 additions and 42 deletions

View File

@ -74,7 +74,7 @@ func (c *connectionService) Stop(ctx context.Context) {
c.connMap = map[string]connectionItem{} 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 var sshClient *ssh.Client
if config.SSH.Enable { if config.SSH.Enable {
sshConfig := &ssh.ClientConfig{ sshConfig := &ssh.ClientConfig{
@ -127,10 +127,21 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*r
option.ReadTimeout = -2 option.ReadTimeout = -2
option.WriteTimeout = -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 { if config.Sentinel.Enable {
sentinel := redis.NewSentinelClient(option) 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 { if err != nil {
return nil, err return nil, err
} }
@ -146,6 +157,40 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*r
return rdb, nil 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) { func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp types.JSResp) {
rdb, err := c.createRedisClient(config) rdb, err := c.createRedisClient(config)
if err != nil { if err != nil {
@ -153,7 +198,8 @@ func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp
return return
} }
defer rdb.Close() 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() resp.Msg = err.Error()
} else { } else {
resp.Success = true resp.Success = true

View File

@ -195,8 +195,7 @@ func (c *ConnectionsStorage) UpdateConnection(name string, param types.Connectio
updated = true updated = true
} }
} else { } else {
err := retrieve(conn.Connections, name, param) if err := retrieve(conn.Connections, name, param); err != nil {
if err != nil {
return err return err
} }
} }
@ -287,7 +286,7 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
return c.saveConnections(conns) return c.saveConnections(conns)
} }
// CreateGroup create new group // CreateGroup create a new group
func (c *ConnectionsStorage) CreateGroup(name string) error { func (c *ConnectionsStorage) CreateGroup(name string) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
@ -333,7 +332,7 @@ func (c *ConnectionsStorage) RenameGroup(name, newName string) error {
return c.saveConnections(conns) 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 { func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()

View File

@ -45,7 +45,7 @@ type ConnectionSSH struct {
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"`
LoginType string `json:"loginType" yaml:"login_type"` 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"` Password string `json:"password,omitempty" yaml:"password,omitempty"`
PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"` PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"`
Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"` Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"`

View File

@ -2,7 +2,7 @@
import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash' import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash'
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' 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 useDialog, { ConnDialogType } from 'stores/dialog'
import Close from '@/components/icons/Close.vue' import Close from '@/components/icons/Close.vue'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
@ -57,17 +57,27 @@ const groupOptions = computed(() => {
}) })
const dbFilterList = ref([]) const dbFilterList = ref([])
const onUpdateDBFilterList = (list) => { const onUpdateDBFilterType = (t) => {
const dbList = [] if (t !== 'none') {
for (const item of list) { // set default filter index if empty
const idx = toNumber(item) if (isEmpty(dbFilterList.value)) {
if (!isNaN(idx)) { dbFilterList.value = ['0']
dbList.push(idx)
} }
} }
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(() => { const sshLoginType = computed(() => {
return get(generalForm.value, 'ssh.loginType', 'pwd') 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 tab = ref('general')
const testing = ref(false) const testing = ref(false)
const showTestResult = 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 // trim ssh login data
if (generalForm.value.ssh.enable) { if (generalForm.value.ssh.enable) {
switch (generalForm.value.ssh.loginType) { switch (generalForm.value.ssh.loginType) {
@ -121,6 +166,13 @@ const onSaveConnection = async () => {
generalForm.value.ssh = ssh 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 // store new connection
const { success, msg } = await connectionStore.saveConnection( const { success, msg } = await connectionStore.saveConnection(
isEditMode.value ? editName.value : null, isEditMode.value ? editName.value : null,
@ -142,6 +194,7 @@ const resetForm = () => {
showTestResult.value = false showTestResult.value = false
testResult.value = '' testResult.value = ''
tab.value = 'general' tab.value = 'general'
loadingSentinelMaster.value = false
} }
watch( watch(
@ -286,7 +339,9 @@ const onClose = () => {
</n-input-number> </n-input-number>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_type')"> <n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_type')">
<n-radio-group v-model:value="generalForm.dbFilterType"> <n-radio-group
v-model:value="generalForm.dbFilterType"
@update:value="onUpdateDBFilterType">
<n-radio-button :label="$t('dialogue.connection.advn.dbfilter_all')" value="none" /> <n-radio-button :label="$t('dialogue.connection.advn.dbfilter_all')" value="none" />
<n-radio-button <n-radio-button
:label="$t('dialogue.connection.advn.dbfilter_show')" :label="$t('dialogue.connection.advn.dbfilter_show')"
@ -296,7 +351,10 @@ const onClose = () => {
value="hide" /> value="hide" />
</n-radio-group> </n-radio-group>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_input')"> <n-form-item-gi
v-show="generalForm.dbFilterType !== 'none'"
:span="24"
:label="$t('dialogue.connection.advn.dbfilter_input')">
<n-select <n-select
v-model:value="dbFilterList" v-model:value="dbFilterList"
:disabled="generalForm.dbFilterType === 'none'" :disabled="generalForm.dbFilterType === 'none'"
@ -306,8 +364,7 @@ const onClose = () => {
:placeholder="$t('dialogue.connection.advn.dbfilter_input_tip')" :placeholder="$t('dialogue.connection.advn.dbfilter_input_tip')"
:show-arrow="false" :show-arrow="false"
:show="false" :show="false"
:clearable="true" :clearable="true" />
@update:value="onUpdateDBFilterList" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
:span="24" :span="24"
@ -403,9 +460,16 @@ const onClose = () => {
:disabled="!generalForm.sentinel.enable" :disabled="!generalForm.sentinel.enable"
label-placement="top"> label-placement="top">
<n-form-item :label="$t('dialogue.connection.sentinel.master')"> <n-form-item :label="$t('dialogue.connection.sentinel.master')">
<n-input <n-space>
v-model:value="generalForm.sentinel.master" <n-select
:placeholder="$t('dialogue.connection.sentinel.master')" /> v-model:value="generalForm.sentinel.master"
filterable
tag
:options="masterNameOptions" />
<n-button :loading="loadingSentinelMaster" @click="onLoadSentinelMasters">
{{ $t('dialogue.connection.sentinel.auto_discover') }}
</n-button>
</n-space>
</n-form-item> </n-form-item>
<n-form-item :label="$t('dialogue.connection.sentinel.password')"> <n-form-item :label="$t('dialogue.connection.sentinel.password')">
<n-input <n-input
@ -423,7 +487,6 @@ const onClose = () => {
</n-tab-pane> </n-tab-pane>
<!-- TODO: SSL tab pane --> <!-- TODO: SSL tab pane -->
<!-- TODO: Sentinel tab pane -->
<!-- TODO: Cluster tab pane --> <!-- TODO: Cluster tab pane -->
</n-tabs> </n-tabs>

View File

@ -122,8 +122,8 @@
"pwd": "Password", "pwd": "Password",
"name_tip": "Connection name", "name_tip": "Connection name",
"addr_tip": "Redis server host", "addr_tip": "Redis server host",
"usr_tip": "(Optional) Redis server username", "usr_tip": "(Optional) Authentication username",
"pwd_tip": "(Optional) Redis server authentication password (Redis > 6.0)", "pwd_tip": "(Optional) Authentication password (Redis > 6.0)",
"test": "Test Connection", "test": "Test Connection",
"test_succ": "Successful connection to redis-server", "test_succ": "Successful connection to redis-server",
"test_fail": "Fail Connection", "test_fail": "Fail Connection",
@ -161,11 +161,12 @@
"sentinel": { "sentinel": {
"title": "Sentinel", "title": "Sentinel",
"enable": "Serve as Sentinel Node", "enable": "Serve as Sentinel Node",
"master": "Name of Master Node", "master": "Master Group Name",
"auto_discover": "Auto Discover",
"password": "Password for Master Node", "password": "Password for Master Node",
"username": "Username for Master Node", "username": "Username for Master Node",
"pwd_tip": "(Optional) username for master node", "pwd_tip": "(Optional) Authentication username for master node",
"usr_tip": "(Optional) authentication password for master node (Redis > 6.0)" "usr_tip": "(Optional) Authentication password for master node (Redis > 6.0)"
} }
}, },
"group": { "group": {

View File

@ -132,7 +132,7 @@
"filter": "默认键过滤表达式", "filter": "默认键过滤表达式",
"filter_tip": "需要加载的键名表达式", "filter_tip": "需要加载的键名表达式",
"separator": "键分隔符", "separator": "键分隔符",
"separator_tip":"键名路径分隔符", "separator_tip": "键名路径分隔符",
"conn_timeout": "连接超时", "conn_timeout": "连接超时",
"exec_timeout": "执行超时", "exec_timeout": "执行超时",
"dbfilter_type": "数据库过滤方式", "dbfilter_type": "数据库过滤方式",
@ -161,7 +161,8 @@
"sentinel": { "sentinel": {
"title": "哨兵模式", "title": "哨兵模式",
"enable": "当前为哨兵节点", "enable": "当前为哨兵节点",
"master": "主实例名", "master": "主节点组名",
"auto_discover": "自动查询组名",
"password": "主节点密码", "password": "主节点密码",
"username": "主节点用户名", "username": "主节点用户名",
"pwd_tip": "(可选)主节点服务授权用户名", "pwd_tip": "(可选)主节点服务授权用户名",
@ -209,7 +210,7 @@
"ttl": { "ttl": {
"title": "设置键存活时间" "title": "设置键存活时间"
}, },
"upgrade":{ "upgrade": {
"title": "有可用新版本", "title": "有可用新版本",
"new_version_tip": "新版本({ver}),是否立即下载", "new_version_tip": "新版本({ver}),是否立即下载",
"no_update": "当前已是最新版", "no_update": "当前已是最新版",

View File

@ -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 * get database server by name
* @param name * @param name

View File

@ -82,16 +82,7 @@ const useDialogStore = defineStore('dialog', {
async openEditDialog(name) { async openEditDialog(name) {
const connStore = useConnectionStore() const connStore = useConnectionStore()
const profile = await connStore.getConnectionProfile(name) const profile = await connStore.getConnectionProfile(name)
const assignCustomizer = (objVal, srcVal, key) => { this.connParam = connStore.mergeConnectionProfile(connStore.newDefaultConnection(name), profile)
if (isEmpty(objVal)) {
return srcVal
}
if (isEmpty(srcVal)) {
return objVal
}
return undefined
}
this.connParam = assignWith({}, connStore.newDefaultConnection(name), profile, assignCustomizer)
this.connType = ConnDialogType.EDIT this.connType = ConnDialogType.EDIT
this.connDialogVisible = true this.connDialogVisible = true
}, },