Compare commits

...

6 Commits

16 changed files with 278 additions and 13 deletions

View File

@ -591,7 +591,12 @@ func (b *browserService) GetKeyType(param types.KeySummaryParam) (resp types.JSR
}
var data types.KeySummary
data.Type = strings.ToLower(keyType)
switch keyType {
case "ReJSON-RL":
data.Type = "JSON"
default:
data.Type = strings.ToLower(keyType)
}
resp.Success = true
resp.Data = data
@ -624,7 +629,7 @@ func (b *browserService) GetKeySummary(param types.KeySummaryParam) (resp types.
}
size, _ := client.MemoryUsage(ctx, key, 0).Result()
data := types.KeySummary{
Type: strings.ToLower(typeVal.Val()),
Type: typeVal.Val(),
Size: size,
}
if data.Type == "none" {
@ -655,6 +660,9 @@ func (b *browserService) GetKeySummary(param types.KeySummaryParam) (resp types.
data.Length, err = client.ZCard(ctx, key).Result()
case "stream":
data.Length, err = client.XLen(ctx, key).Result()
case "ReJSON-RL":
data.Type = "JSON"
data.Length = 0
default:
err = errors.New("unknown key type")
}
@ -1091,6 +1099,12 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
resp.Msg = err.Error()
return
}
case "rejson-rl":
var jsonStr string
data.KeyType = "JSON"
jsonStr, err = client.JSONGet(ctx, key).Result()
data.Value, data.Decode, data.Format = convutil.ConvertTo(jsonStr, types.DECODE_NONE, types.FORMAT_JSON, nil)
}
if err != nil {
resp.Msg = err.Error()
@ -1235,6 +1249,11 @@ func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp
}
}
}
case "json":
err = client.JSONSet(ctx, key, ".", param.Value).Err()
if err == nil && expiration > 0 {
client.Expire(ctx, key, expiration)
}
}
if err != nil {

View File

@ -144,9 +144,6 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
option.Addr = fmt.Sprintf("%s:%d", config.Addr, config.Port)
}
}
if config.LastDB > 0 {
option.DB = config.LastDB
}
if sshClient != nil {
option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
return sshClient.Dial(network, addr)
@ -181,6 +178,10 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (re
option.Password = config.Sentinel.Password
}
if config.LastDB > 0 {
option.DB = config.LastDB
}
rdb := redis.NewClient(option)
if config.Cluster.Enable {
// connect to cluster

View File

@ -21,7 +21,7 @@ type ConnectionConfig struct {
KeyView int `json:"keyView,omitempty" yaml:"key_view,omitempty"`
LoadSize int `json:"loadSize,omitempty" yaml:"load_size,omitempty"`
MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"`
RefreshInterval int `json:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty"`
RefreshInterval int `json:"refreshInterval,omitempty" yaml:"refresh_interval,omitempty"`
Alias map[int]string `json:"alias,omitempty" yaml:"alias,omitempty"`
SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"`
SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`
@ -48,10 +48,10 @@ type ConnectionDB struct {
type ConnectionSSL struct {
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
KeyFile string `json:"keyFile,omitempty" yaml:"keyFile,omitempty"`
CertFile string `json:"certFile,omitempty" yaml:"certFile,omitempty"`
CAFile string `json:"caFile,omitempty" yaml:"caFile,omitempty"`
AllowInsecure bool `json:"allowInsecure,omitempty" yaml:"allowInsecure,omitempty"`
KeyFile string `json:"keyFile,omitempty" yaml:"keyfile,omitempty"`
CertFile string `json:"certFile,omitempty" yaml:"certfile,omitempty"`
CAFile string `json:"caFile,omitempty" yaml:"cafile,omitempty"`
AllowInsecure bool `json:"allowInsecure,omitempty" yaml:"allow_insecure,omitempty"`
SNI string `json:"sni,omitempty" yaml:"sni,omitempty"`
}

View File

@ -30,6 +30,7 @@ func NewPreferences() Preferences {
ShowLineNum: true,
ShowFolding: true,
DropText: true,
Links: true,
},
Cli: PreferencesCli{
FontSize: consts.DEFAULT_FONT_SIZE,
@ -69,6 +70,7 @@ type PreferencesEditor struct {
ShowLineNum bool `json:"showLineNum" yaml:"show_line_num"`
ShowFolding bool `json:"showFolding" yaml:"show_folding"`
DropText bool `json:"dropText" yaml:"drop_text"`
Links bool `json:"links" yaml:"links"`
}
type PreferencesCli struct {

View File

@ -56,6 +56,7 @@ onMounted(async () => {
theme: pref.isDark ? 'rdm-dark' : 'rdm-light',
language: props.language,
lineNumbers: pref.showLineNum ? 'on' : 'off',
links: pref.editorLinks,
readOnly: readonlyValue.value,
colorDecorators: true,
accessibilitySupport: 'off',
@ -145,7 +146,7 @@ watch(
watch(
() => pref.editor,
({ showLineNum = true, showFolding = true, dropText = true }) => {
({ showLineNum = true, showFolding = true, dropText = true, links = true }) => {
if (editorNode != null) {
const { fontSize, fontFamily } = pref.editorFont
editorNode.updateOptions({
@ -154,6 +155,7 @@ watch(
lineNumbers: showLineNum ? 'on' : 'off',
folding: showFolding,
dragAndDrop: dropText,
links,
})
}
},

View File

@ -0,0 +1,177 @@
<script setup>
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Copy from '@/components/icons/Copy.vue'
import Save from '@/components/icons/Save.vue'
import { useThemeVars } from 'naive-ui'
import { types as redisTypes } from '@/consts/support_redis_type.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { isEmpty, toLower } from 'lodash'
import useBrowserStore from 'stores/browser.js'
import { decodeRedisKey } from '@/utils/key_convert.js'
import ContentEditor from '@/components/content_value/ContentEditor.vue'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { formatBytes } from '@/utils/byte_convert.js'
const props = defineProps({
name: String,
db: Number,
keyPath: String,
keyCode: {
type: Array,
default: null,
},
ttl: {
type: Number,
default: -1,
},
value: String,
size: Number,
length: Number,
loading: Boolean,
})
const i18n = useI18n()
const themeVars = useThemeVars()
/**
*
* @type {ComputedRef<string|number[]>}
*/
const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
})
const keyType = redisTypes.JSON
const editingContent = ref('')
const displayValue = computed(() => {
if (props.loading) {
return ''
}
return decodeRedisKey(props.value)
})
const enableSave = computed(() => {
return editingContent.value !== displayValue.value && !props.loading
})
const showMemoryUsage = computed(() => {
return !isNaN(props.size) && props.size > 0
})
/**
* Copy value
*/
const onCopyValue = () => {
ClipboardSetText(displayValue.value)
.then((succ) => {
if (succ) {
$message.success(i18n.t('interface.copy_succ'))
}
})
.catch((e) => {
$message.error(e.message)
})
}
/**
* Save value
*/
const browserStore = useBrowserStore()
const saving = ref(false)
const onInput = (content) => {
editingContent.value = content
}
const onSave = async () => {
saving.value = true
try {
const { success, msg } = await browserStore.setKey({
server: props.name,
db: props.db,
key: keyName.value,
keyType: toLower(keyType),
value: editingContent.value,
ttl: -1,
format: formatTypes.JSON,
decode: decodeTypes.NONE,
})
if (success) {
$message.success(i18n.t('interface.save_value_succ'))
} else {
$message.error(msg)
}
} catch (e) {
$message.error(e.message)
} finally {
saving.value = false
}
}
defineExpose({
reset: () => {
editingContent.value = ''
},
})
</script>
<template>
<div class="content-wrapper flex-box-v">
<slot name="toolbar" />
<div class="tb2 value-item-part flex-box-h">
<div class="flex-item-expand"></div>
<n-button-group>
<n-button :disabled="saving" :focusable="false" @click="onCopyValue">
<template #icon>
<n-icon :component="Copy" size="18" />
</template>
{{ $t('interface.copy_value') }}
</n-button>
<n-button
:disabled="!enableSave"
:loading="saving"
:secondary="enableSave"
:type="enableSave ? 'primary' : ''"
@click="onSave">
<template #icon>
<n-icon :component="Save" size="18" />
</template>
{{ $t('common.save') }}
</n-button>
</n-button-group>
</div>
<div class="value-wrapper value-item-part flex-item-expand flex-box-v">
<n-spin :show="props.loading" />
<content-editor
v-show="!props.loading"
:content="displayValue"
:loading="props.loading"
class="flex-item-expand"
language="json"
style="height: 100%"
@input="onInput"
@reset="onInput"
@save="onSave" />
</div>
<div class="value-footer flex-box-h">
<n-text v-if="showMemoryUsage">{{ $t('interface.memory_usage') }}: {{ formatBytes(props.size) }}</n-text>
<div class="flex-item-expand" />
</div>
</div>
</template>
<style lang="scss" scoped>
.value-wrapper {
//overflow: hidden;
border-top: v-bind('themeVars.borderColor') 1px solid;
padding: 5px;
}
.value-footer {
border-top: v-bind('themeVars.borderColor') 1px solid;
background-color: v-bind('themeVars.tableHeaderColor');
}
</style>

View File

@ -14,6 +14,7 @@ import useDialogStore from 'stores/dialog.js'
import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { useI18n } from 'vue-i18n'
import ContentToolbar from '@/components/content_value/ContentToolbar.vue'
import ContentValueJson from '@/components/content_value/ContentValueJson.vue'
const themeVars = useThemeVars()
const browserStore = useBrowserStore()
@ -66,6 +67,7 @@ const valueComponents = {
[redisTypes.SET]: ContentValueSet,
[redisTypes.ZSET]: ContentValueZset,
[redisTypes.STREAM]: ContentValueStream,
[redisTypes.JSON]: ContentValueJson,
}
const keyName = computed(() => {

View File

@ -164,6 +164,16 @@ const onSaveConnection = async () => {
}
})
// trim addr by network type
if (get(generalForm.value, 'network', 'tcp') === 'unix') {
generalForm.value.network = 'unix'
generalForm.value.addr = ''
generalForm.value.port = 0
} else {
generalForm.value.network = ''
generalForm.value.sock = ''
}
// trim advance data
if (get(generalForm.value, 'dbFilterType', 'none') === 'none') {
generalForm.value.dbFilterList = []

View File

@ -14,6 +14,7 @@ import useTabStore from 'stores/tab.js'
import NewStreamValue from '@/components/new_value/NewStreamValue.vue'
import useBrowserStore from 'stores/browser.js'
import Import from '@/components/icons/Import.vue'
import NewJsonValue from '@/components/new_value/NewJsonValue.vue'
const i18n = useI18n()
const newForm = reactive({
@ -54,6 +55,7 @@ const newValueComponent = {
[types.SET]: NewSetValue,
[types.ZSET]: NewZSetValue,
[types.STREAM]: NewStreamValue,
[types.JSON]: NewJsonValue,
}
const defaultValue = {
[types.STRING]: '',
@ -62,6 +64,7 @@ const defaultValue = {
[types.SET]: [],
[types.ZSET]: [],
[types.STREAM]: [],
[types.JSON]: '{}',
}
const dialogStore = useDialog()

View File

@ -301,6 +301,11 @@ const onClose = () => {
{{ $t('preferences.editor.drop_text') }}
</n-checkbox>
</n-form-item-gi>
<n-form-item-gi :show-feedback="false" :show-label="false" :span="24">
<n-checkbox v-model:checked="prefStore.editor.links">
{{ $t('preferences.editor.links') }}
</n-checkbox>
</n-form-item-gi>
</n-grid>
</n-form>
</n-tab-pane>

View File

@ -0,0 +1,20 @@
<script setup>
const props = defineProps({
value: String,
})
const emit = defineEmits(['update:value'])
</script>
<template>
<n-form-item :label="$t('common.value')">
<n-input
:rows="6"
:value="props.value"
placeholder=""
type="textarea"
@input="(val) => emit('update:value', val)" />
</n-form-item>
</template>
<style lang="scss" scoped></style>

View File

@ -9,6 +9,7 @@ export const types = {
SET: 'SET',
ZSET: 'ZSET',
STREAM: 'STREAM',
JSON: 'JSON',
}
export const typesShortName = {
@ -18,6 +19,7 @@ export const typesShortName = {
SET: 'E',
ZSET: 'Z',
STREAM: 'X',
JSON: 'J',
}
/**
@ -31,6 +33,7 @@ export const typesColor = {
[types.SET]: '#F59E0B',
[types.ZSET]: '#EF4444',
[types.STREAM]: '#EC4899',
[types.JSON]: '#374254',
}
/**
@ -44,6 +47,7 @@ export const typesBgColor = {
[types.SET]: '#FDF1DF',
[types.ZSET]: '#FAEAED',
[types.STREAM]: '#FDE6F1',
[types.JSON]: '#C1C1D3',
}
// export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name]))

View File

@ -55,7 +55,8 @@
"name": "Editor",
"show_linenum": "Show Line Numbers",
"show_folding": "Enable Code Folding",
"drop_text": "Allow Drag and Drop Text"
"drop_text": "Allow Drag and Drop Text",
"links": "Support links"
},
"cli": {
"name": "Command Line",

View File

@ -55,7 +55,8 @@
"name": "编辑器",
"show_linenum": "显示行号",
"show_folding": "启用代码折叠",
"drop_text": "允许拖放文本"
"drop_text": "允许拖放文本",
"links": "支持连接跳转"
},
"cli": {
"name": "命令行",

View File

@ -59,6 +59,7 @@ const usePreferencesStore = defineStore('preferences', {
showLineNum: true,
showFolding: true,
dropText: true,
links: true,
},
cli: {
fontFamily: [],
@ -259,6 +260,10 @@ const usePreferencesStore = defineStore('preferences', {
return get(this.editor, 'dropText', true)
},
editorLinks() {
return get(this.editor, 'links', true)
},
keyIconType() {
return get(this.general, 'keyIconStyle', typesIconStyle.SHORT)
},
@ -292,6 +297,10 @@ const usePreferencesStore = defineStore('preferences', {
if (dropText === undefined) {
set(data, 'editor.dropText', true)
}
const links = get(data, 'editor.links')
if (links === undefined) {
set(data, 'editor.links', true)
}
i18nGlobal.locale.value = this.currentLanguage
}
},

View File

@ -3,6 +3,7 @@ import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
export const setupMonaco = () => {
window.MonacoEnvironment = {
@ -40,4 +41,12 @@ export const setupMonaco = () => {
rules: [],
colors: {},
})
// register default link opening behavior
monaco.editor.registerLinkOpener({
open(resource) {
BrowserOpenURL(resource.toString())
return true
},
})
}