Compare commits

...

2 Commits

Author SHA1 Message Date
tiny-craft 66ac7194e2 refactor: define preferences data with go struct 2023-10-04 22:21:35 +08:00
tiny-craft ffc50a7fcc refactor: move the handling of String viewing methods to backend
feat: add view as gzip/deflate #30
2023-10-04 04:42:17 +08:00
16 changed files with 491 additions and 283 deletions

View File

@ -17,6 +17,7 @@ import (
"tinyrdm/backend/types"
maputil "tinyrdm/backend/utils/map"
redis2 "tinyrdm/backend/utils/redis"
strutil "tinyrdm/backend/utils/string"
)
type cmdHistoryItem struct {
@ -476,7 +477,7 @@ func (c *connectionService) ScanKeys(connName string, db int, match, keyType str
}
// GetKeyValue get value by key
func (c *connectionService) GetKeyValue(connName string, db int, key string) (resp types.JSResp) {
func (c *connectionService) GetKeyValue(connName string, db int, key, viewAs string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db)
if err != nil {
resp.Msg = err.Error()
@ -512,7 +513,9 @@ func (c *connectionService) GetKeyValue(connName string, db int, key string) (re
var cursor uint64
switch strings.ToLower(keyType) {
case "string":
value, err = rdb.Get(ctx, key).Result()
var str string
str, err = rdb.Get(ctx, key).Result()
value, viewAs = strutil.ConvertTo(str, viewAs)
size, _ = rdb.StrLen(ctx, key).Result()
case "list":
value, err = rdb.LRange(ctx, key, 0, -1).Result()
@ -605,13 +608,14 @@ func (c *connectionService) GetKeyValue(connName string, db int, key string) (re
"ttl": ttl,
"value": value,
"size": size,
"viewAs": viewAs,
}
return
}
// SetKeyValue set value by key
// @param ttl <= 0 means keep current ttl
func (c *connectionService) SetKeyValue(connName string, db int, key, keyType string, value any, ttl int64) (resp types.JSResp) {
func (c *connectionService) SetKeyValue(connName string, db int, key, keyType string, value any, ttl int64, viewAs string) (resp types.JSResp) {
rdb, ctx, err := c.getRedisClient(connName, db)
if err != nil {
resp.Msg = err.Error()
@ -632,7 +636,12 @@ func (c *connectionService) SetKeyValue(connName string, db int, key, keyType st
resp.Msg = "invalid string value"
return
} else {
_, err = rdb.Set(ctx, key, str, 0).Result()
var saveStr string
if saveStr, err = strutil.SaveAs(str, viewAs); err != nil {
resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error())
return
}
_, err = rdb.Set(ctx, key, saveStr, 0).Result()
// set expiration lonely, not "keepttl"
if err == nil && expiration > 0 {
rdb.Expire(ctx, key, expiration)

View File

@ -39,8 +39,8 @@ func (p *preferencesService) GetPreferences() (resp types.JSResp) {
return
}
func (p *preferencesService) SetPreferences(values map[string]any) (resp types.JSResp) {
err := p.pref.SetPreferencesN(values)
func (p *preferencesService) SetPreferences(pf types.Preferences) (resp types.JSResp) {
err := p.pref.SetPreferences(&pf)
if err != nil {
resp.Msg = err.Error()
return
@ -50,6 +50,16 @@ func (p *preferencesService) SetPreferences(values map[string]any) (resp types.J
return
}
func (p *preferencesService) UpdatePreferences(value map[string]any) (resp types.JSResp) {
err := p.pref.UpdatePreferences(value)
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true
return
}
func (p *preferencesService) RestorePreferences() (resp types.JSResp) {
defaultPref := p.pref.RestoreDefault()
resp.Data = map[string]any{
@ -103,23 +113,20 @@ func (p *preferencesService) GetAppVersion() (resp types.JSResp) {
}
func (p *preferencesService) SaveWindowSize(width, height int) {
p.SetPreferences(map[string]any{
"behavior": map[string]any{
"window_width": width,
"window_height": height,
},
p.UpdatePreferences(map[string]any{
"behavior.windowWidth": width,
"behavior.windowHeight": height,
})
}
func (p *preferencesService) GetWindowSize() (width, height int) {
data := p.pref.GetPreferences()
w, h := data["behavior.window_width"], data["behavior.window_height"]
var ok bool
if width, ok = w.(int); !ok {
return consts.DEFAULT_WINDOW_WIDTH, consts.DEFAULT_WINDOW_HEIGHT
width, height = data.Behavior.WindowWidth, data.Behavior.WindowHeight
if width <= 0 {
width = consts.DEFAULT_WINDOW_WIDTH
}
if height, ok = h.(int); !ok {
return consts.DEFAULT_WINDOW_WIDTH, consts.DEFAULT_WINDOW_HEIGHT
if height <= 0 {
height = consts.DEFAULT_WINDOW_HEIGHT
}
return
}

View File

@ -3,9 +3,11 @@ package storage
import (
"fmt"
"gopkg.in/yaml.v3"
"log"
"reflect"
"strings"
"sync"
"tinyrdm/backend/consts"
"tinyrdm/backend/types"
)
type PreferencesStorage struct {
@ -19,30 +21,11 @@ func NewPreferences() *PreferencesStorage {
}
}
func (p *PreferencesStorage) DefaultPreferences() map[string]any {
return map[string]any{
"behavior": map[string]any{
"aside_width": consts.DEFAULT_ASIDE_WIDTH,
"window_width": consts.DEFAULT_WINDOW_WIDTH,
"window_height": consts.DEFAULT_WINDOW_HEIGHT,
},
"general": map[string]any{
"language": "auto",
"font": "",
"font_size": consts.DEFAULT_FONT_SIZE,
"use_sys_proxy": false,
"use_sys_proxy_http": false,
"check_update": true,
"skip_version": "",
},
"editor": map[string]any{
"font": "",
"font_size": 14,
},
}
func (p *PreferencesStorage) DefaultPreferences() types.Preferences {
return types.NewPreferences()
}
func (p *PreferencesStorage) getPreferences() (ret map[string]any) {
func (p *PreferencesStorage) getPreferences() (ret types.Preferences) {
b, err := p.storage.Load()
if err != nil {
ret = p.DefaultPreferences()
@ -56,79 +39,43 @@ func (p *PreferencesStorage) getPreferences() (ret map[string]any) {
return
}
func (p *PreferencesStorage) flatPreferences(data map[string]any, prefix string) map[string]any {
flattened := make(map[string]any)
for key, value := range data {
newKey := key
if prefix != "" {
newKey = prefix + "." + key
}
if nested, ok := value.(map[string]any); ok {
nestedFlattened := p.flatPreferences(nested, newKey)
for k, v := range nestedFlattened {
flattened[k] = v
}
} else {
flattened[newKey] = value
}
}
return flattened
}
// GetPreferences Get preferences from local
func (p *PreferencesStorage) GetPreferences() (ret map[string]any) {
func (p *PreferencesStorage) GetPreferences() (ret types.Preferences) {
p.mutex.Lock()
defer p.mutex.Unlock()
pref := p.getPreferences()
ret = p.flatPreferences(pref, "")
ret = p.getPreferences()
return
}
func (p *PreferencesStorage) Value(keys ...string) any {
kv := p.getPreferences()
var ok bool
var a any
total := len(keys)
for i, key := range keys {
if a, ok = kv[key]; !ok {
return nil
}
if i == total-1 {
// last key, return value
return a
}
if kv, ok = a.(map[string]any); !ok {
return nil
}
}
return nil
}
func (p *PreferencesStorage) setPreferences(pf map[string]any, key string, value any) error {
keyPath := strings.Split(key, ".")
if len(keyPath) <= 0 {
return fmt.Errorf("invalid key path(%s)", key)
}
var node any = pf
for _, k := range keyPath[:len(keyPath)-1] {
if subNode, ok := node.(map[string]any); ok {
node = subNode[k]
func (p *PreferencesStorage) setPreferences(pf *types.Preferences, key string, value any) error {
parts := strings.Split(key, ".")
if len(parts) > 0 {
var reflectValue reflect.Value
if reflect.TypeOf(pf).Kind() == reflect.Ptr {
reflectValue = reflect.ValueOf(pf).Elem()
} else {
return fmt.Errorf("invalid key path(%s)", key)
reflectValue = reflect.ValueOf(pf)
}
}
if subNode, ok := node.(map[string]any); ok {
subNode[keyPath[len(keyPath)-1]] = value
}
for i, part := range parts {
part = strings.ToUpper(part[:1]) + part[1:]
reflectValue = reflectValue.FieldByName(part)
if reflectValue.IsValid() {
if i == len(parts)-1 {
reflectValue.Set(reflect.ValueOf(value))
return nil
}
} else {
break
}
}
}
func (p *PreferencesStorage) savePreferences(pf map[string]any) error {
b, err := yaml.Marshal(&pf)
return fmt.Errorf("invalid key path(%s)", key)
}
func (p *PreferencesStorage) savePreferences(pf *types.Preferences) error {
b, err := yaml.Marshal(pf)
if err != nil {
return err
}
@ -139,35 +86,35 @@ func (p *PreferencesStorage) savePreferences(pf map[string]any) error {
return nil
}
// SetPreferences assign value to key path, the key path use "." to indicate multiple level
func (p *PreferencesStorage) SetPreferences(key string, value any) error {
// SetPreferences replace preferences
func (p *PreferencesStorage) SetPreferences(pf *types.Preferences) error {
p.mutex.Lock()
defer p.mutex.Unlock()
pf := p.getPreferences()
if err := p.setPreferences(pf, key, value); err != nil {
return err
}
return p.savePreferences(pf)
}
// SetPreferencesN set multiple key path and value
func (p *PreferencesStorage) SetPreferencesN(values map[string]any) error {
// UpdatePreferences update values by key paths, the key path use "." to indicate multiple level
func (p *PreferencesStorage) UpdatePreferences(values map[string]any) error {
p.mutex.Lock()
defer p.mutex.Unlock()
pf := p.getPreferences()
for path, v := range values {
if err := p.setPreferences(pf, path, v); err != nil {
if err := p.setPreferences(&pf, path, v); err != nil {
return err
}
}
log.Println("after save", pf)
return p.savePreferences(pf)
return p.savePreferences(&pf)
}
func (p *PreferencesStorage) RestoreDefault() map[string]any {
func (p *PreferencesStorage) RestoreDefault() types.Preferences {
p.mutex.Lock()
defer p.mutex.Unlock()
pf := p.DefaultPreferences()
p.savePreferences(pf)
return p.flatPreferences(pf, "")
p.savePreferences(&pf)
return pf
}

View File

@ -0,0 +1,50 @@
package types
import "tinyrdm/backend/consts"
type Preferences struct {
Behavior PreferencesBehavior `json:"behavior" yaml:"behavior"`
General PreferencesGeneral `json:"general" yaml:"general"`
Editor PreferencesEditor `json:"editor" yaml:"editor"`
}
func NewPreferences() Preferences {
return Preferences{
Behavior: PreferencesBehavior{
AsideWidth: consts.DEFAULT_ASIDE_WIDTH,
WindowWidth: consts.DEFAULT_WINDOW_WIDTH,
WindowHeight: consts.DEFAULT_WINDOW_HEIGHT,
},
General: PreferencesGeneral{
Theme: "auto",
Language: "auto",
FontSize: consts.DEFAULT_FONT_SIZE,
CheckUpdate: true,
},
Editor: PreferencesEditor{
FontSize: consts.DEFAULT_FONT_SIZE,
},
}
}
type PreferencesBehavior struct {
AsideWidth int `json:"asideWidth" yaml:"aside_width"`
WindowWidth int `json:"windowWidth" yaml:"window_width"`
WindowHeight int `json:"windowHeight" yaml:"window_height"`
}
type PreferencesGeneral struct {
Theme string `json:"theme" yaml:"theme"`
Language string `json:"language" yaml:"language"`
Font string `json:"font" yaml:"font,omitempty"`
FontSize int `json:"fontSize" yaml:"font_size"`
UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"`
UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"`
CheckUpdate bool `json:"checkUpdate" yaml:"check_update"`
SkipVersion string `json:"skipVersion" yaml:"skip_version,omitempty"`
}
type PreferencesEditor struct {
Font string `json:"font" yaml:"font,omitempty"`
FontSize int `json:"fontSize" yaml:"font_size"`
}

View File

@ -0,0 +1,11 @@
package types
const PLAIN_TEXT = "Plain Text"
const JSON = "JSON"
const BASE64_TEXT = "Base64 Text"
const BASE64_JSON = "Base64 JSON"
const HEX = "Hex"
const BINARY = "Binary"
const GZIP = "GZip"
const GZIP_JSON = "GZip JSON"
const DEFLATE = "Deflate"

View File

@ -0,0 +1,275 @@
package strutil
import (
"bytes"
"compress/flate"
"compress/gzip"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"strings"
"tinyrdm/backend/types"
)
// ConvertTo convert string to specified type
// @param targetType empty string indicates automatic detection of the string type
func ConvertTo(str, targetType string) (value, resultType string) {
if len(str) <= 0 {
// empty content
if len(targetType) <= 0 {
resultType = types.PLAIN_TEXT
} else {
resultType = targetType
}
return
}
switch targetType {
case types.PLAIN_TEXT:
value = str
resultType = targetType
return
case types.JSON:
value, _ = decodeJson(str)
resultType = targetType
return
case types.BASE64_TEXT, types.BASE64_JSON:
if base64Str, ok := decodeBase64(str); ok {
if targetType == types.BASE64_JSON {
value, _ = decodeJson(base64Str)
} else {
value = base64Str
}
} else {
value = str
}
resultType = targetType
return
case types.HEX:
if hexStr, ok := decodeHex(str); ok {
log.Print(hexStr)
value = hexStr
} else {
value = str
}
resultType = targetType
return
case types.BINARY:
var binary strings.Builder
for _, char := range str {
binary.WriteString(fmt.Sprintf("%08b", int(char)))
}
value = binary.String()
resultType = targetType
return
case types.GZIP, types.GZIP_JSON:
if gzipStr, ok := decodeGZip(str); ok {
if targetType == types.BASE64_JSON {
value, _ = decodeJson(gzipStr)
} else {
value = gzipStr
}
} else {
value = str
}
resultType = targetType
return
case types.DEFLATE:
value, _ = decodeDeflate(str)
resultType = targetType
return
}
// type isn't specified or unknown, try to automatically detect and return converted value
return autoToType(str)
}
// attempt automatic convert to possible types
// if no conversion is possible, it will return the origin string value and "plain text" type
func autoToType(str string) (value, resultType string) {
if len(str) > 0 {
var ok bool
if value, ok = decodeJson(str); ok {
resultType = types.JSON
return
}
if value, ok = decodeBase64(str); ok {
if value, ok = decodeJson(value); ok {
resultType = types.BASE64_JSON
return
}
resultType = types.BASE64_TEXT
return
}
if value, ok = decodeGZip(str); ok {
resultType = types.GZIP
return
}
if value, ok = decodeDeflate(str); ok {
resultType = types.DEFLATE
return
}
}
value = str
resultType = types.PLAIN_TEXT
return
}
func decodeJson(str string) (string, bool) {
var data any
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
if err := json.Unmarshal([]byte(str), &data); err == nil {
var jsonByte []byte
if jsonByte, err = json.MarshalIndent(data, "", " "); err == nil {
return string(jsonByte), true
}
}
}
return str, false
}
func decodeBase64(str string) (string, bool) {
if decodedStr, err := base64.StdEncoding.DecodeString(str); err == nil {
return string(decodedStr), true
}
return str, false
}
func decodeHex(str string) (string, bool) {
encodeStr := hex.EncodeToString([]byte(str))
var resultStr strings.Builder
for i := 0; i < len(encodeStr); i += 2 {
resultStr.WriteString("\\x")
resultStr.WriteString(encodeStr[i : i+2])
}
return resultStr.String(), true
}
func decodeGZip(str string) (string, bool) {
if reader, err := gzip.NewReader(strings.NewReader(str)); err == nil {
defer reader.Close()
var decompressed []byte
if decompressed, err = io.ReadAll(reader); err == nil {
return string(decompressed), true
}
}
return str, false
}
func decodeDeflate(str string) (string, bool) {
reader := flate.NewReader(strings.NewReader(str))
defer reader.Close()
if decompressed, err := io.ReadAll(reader); err == nil {
return string(decompressed), true
}
return str, false
}
func SaveAs(str, targetType string) (value string, err error) {
switch targetType {
case types.PLAIN_TEXT:
return str, nil
case types.BASE64_TEXT:
base64Str, _ := encodeBase64(str)
return base64Str, nil
case types.JSON, types.BASE64_JSON, types.GZIP_JSON:
if jsonStr, ok := encodeJson(str); ok {
if targetType == types.BASE64_JSON {
base64Str, _ := encodeBase64(jsonStr)
return base64Str, nil
} else {
return jsonStr, nil
}
} else {
return str, errors.New("invalid json")
}
case types.GZIP:
if gzipStr, ok := encodeGZip(str); ok {
return gzipStr, nil
} else {
return str, errors.New("fail to build gzip data")
}
case types.DEFLATE:
if deflateStr, ok := encodeDeflate(str); ok {
return deflateStr, nil
} else {
return str, errors.New("fail to build deflate data")
}
}
return str, errors.New("fail to save with unknown error")
}
func encodeJson(str string) (string, bool) {
var data any
if (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) ||
(strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) {
if err := json.Unmarshal([]byte(str), &data); err == nil {
var jsonByte []byte
if jsonByte, err = json.Marshal(data); err == nil {
return string(jsonByte), true
}
}
}
return str, false
}
func encodeBase64(str string) (string, bool) {
return base64.StdEncoding.EncodeToString([]byte(str)), true
}
func encodeGZip(str string) (string, bool) {
var compress = func(b []byte) (string, error) {
var buf bytes.Buffer
writer := gzip.NewWriter(&buf)
if _, err := writer.Write([]byte(str)); err != nil {
writer.Close()
return "", err
}
writer.Close()
return string(buf.Bytes()), nil
}
if gzipStr, err := compress([]byte(str)); err == nil {
return gzipStr, true
}
return str, false
}
func encodeDeflate(str string) (string, bool) {
var compress = func(b []byte) (string, error) {
var buf bytes.Buffer
writer, err := flate.NewWriter(&buf, flate.DefaultCompression)
if err != nil {
return "", err
}
if _, err = writer.Write([]byte(str)); err != nil {
writer.Close()
return "", err
}
writer.Close()
return string(buf.Bytes()), nil
}
if deflateStr, err := compress([]byte(str)); err == nil {
return deflateStr, true
}
return str, false
}

View File

@ -98,6 +98,7 @@ const tabContent = computed(() => {
ttl: tab.ttl,
value: tab.value,
size: tab.size || 0,
viewAs: tab.viewAs,
}
})
@ -122,7 +123,7 @@ const onReloadKey = async () => {
if (tab == null || isEmpty(tab.key)) {
return null
}
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key)
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key, tab.viewAs)
}
</script>
@ -153,7 +154,8 @@ const onReloadKey = async () => {
:name="tabContent.name"
:ttl="tabContent.ttl"
:value="tabContent.value"
:size="tabContent.size" />
:size="tabContent.size"
:view-as="tabContent.viewAs" />
</div>
</template>

View File

@ -8,12 +8,10 @@ import { useThemeVars } from 'naive-ui'
import { types } from '@/consts/value_view_type.js'
import Close from '@/components/icons/Close.vue'
import Edit from '@/components/icons/Edit.vue'
import { IsJson } from '@/utils/check_string_format.js'
import { types as redisTypes } from '@/consts/support_redis_type.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { map, toLower } from 'lodash'
import useConnectionStore from 'stores/connections.js'
import { fromBase64, fromBase64Json, toBinary, toHex, toJsonText } from '@/utils/string_convert.js'
const i18n = useI18n()
const themeVars = useThemeVars()
@ -28,6 +26,10 @@ const props = defineProps({
},
value: String,
size: Number,
viewAs: {
type: String,
default: types.PLAIN_TEXT,
},
})
const viewOption = computed(() =>
@ -38,15 +40,15 @@ const viewOption = computed(() =>
}
}),
)
const viewAs = ref(types.PLAIN_TEXT)
// const viewAs = ref(types.PLAIN_TEXT)
const autoDetectFormat = () => {
// auto check format when loaded
if (IsJson(props.value)) {
viewAs.value = types.JSON
} else {
viewAs.value = types.PLAIN_TEXT
}
// if (IsJson(props.value)) {
// viewAs.value = types.JSON
// } else {
// viewAs.value = types.PLAIN_TEXT
// }
}
onMounted(() => {
@ -60,44 +62,25 @@ watch(
)
const keyType = redisTypes.STRING
/**
* view value
* @type {ComputedRef<string>}
*/
const viewValue = computed(() => {
switch (viewAs.value) {
case types.PLAIN_TEXT:
return props.value
case types.JSON:
return toJsonText(props.value)
case types.BASE64_TO_TEXT:
return fromBase64(props.value)
case types.BASE64_TO_JSON:
return fromBase64Json(props.value)
case types.HEX:
return toHex(props.value)
case types.BINARY:
return toBinary(props.value)
default:
return props.value
}
})
const viewLanguage = computed(() => {
switch (viewAs.value) {
switch (props.viewAs) {
case types.JSON:
case types.BASE64_TO_JSON:
case types.BASE64_JSON:
return 'json'
default:
return 'plaintext'
}
})
const onViewTypeUpdate = (viewType) => {
connectionStore.loadKeyValue(props.name, props.db, props.keyPath, viewType)
}
/**
* Copy value
*/
const onCopyValue = () => {
ClipboardSetText(viewValue.value)
ClipboardSetText(props.value)
.then((succ) => {
if (succ) {
$message.success(i18n.t('dialogue.copy_succ'))
@ -111,7 +94,7 @@ const onCopyValue = () => {
const editValue = ref('')
const inEdit = ref(false)
const onEditValue = () => {
editValue.value = viewValue.value
editValue.value = props.value
inEdit.value = true
}
@ -134,6 +117,7 @@ const onSaveValue = async () => {
toLower(keyType),
editValue.value,
-1,
props.viewAs,
)
if (success) {
await connectionStore.loadKeyValue(props.name, props.db, props.keyPath)
@ -155,7 +139,11 @@ const onSaveValue = async () => {
<content-toolbar :db="props.db" :key-path="keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
<div class="tb2 flex-box-h">
<n-text>{{ $t('interface.view_as') }}</n-text>
<n-select v-model:value="viewAs" :options="viewOption" style="width: 200px" />
<n-select
:value="props.viewAs"
:options="viewOption"
style="width: 200px"
@update:value="onViewTypeUpdate" />
<div class="flex-item-expand"></div>
<n-button-group v-if="!inEdit">
<n-button :focusable="false" @click="onCopyValue">
@ -188,7 +176,7 @@ const onSaveValue = async () => {
</div>
<div class="value-wrapper flex-item-expand flex-box-v">
<n-scrollbar v-if="!inEdit" class="flex-item-expand">
<n-code :code="viewValue" :language="viewLanguage" show-line-numbers style="cursor: text" word-wrap />
<n-code :code="props.value" :language="viewLanguage" show-line-numbers style="cursor: text" word-wrap />
</n-scrollbar>
<n-input
v-else

View File

@ -1,6 +1,7 @@
<script setup>
import { computed, h, reactive, ref, watch } from 'vue'
import { types, typesColor } from '@/consts/support_redis_type.js'
import { types as viewTypes } from '@/consts/value_view_type.js'
import useDialog from 'stores/dialog'
import { isEmpty, keys, map } from 'lodash'
import NewStringValue from '@/components/new_value/NewStringValue.vue'
@ -116,7 +117,15 @@ const onAdd = async () => {
if (value == null) {
value = defaultValue[type]
}
const { success, msg, nodeKey } = await connectionStore.setKey(server, db, key, type, value, ttl)
const { success, msg, nodeKey } = await connectionStore.setKey(
server,
db,
key,
type,
value,
ttl,
viewTypes.PLAIN_TEXT,
)
if (success) {
// select current key
tabStore.setSelectedKeys(server, nodeKey)

View File

@ -5,8 +5,11 @@
export const types = {
PLAIN_TEXT: 'Plain Text',
JSON: 'JSON',
BASE64_TO_TEXT: 'Base64 To Text',
BASE64_TO_JSON: 'Base64 To JSON',
BASE64_TEXT: 'Base64 Text',
BASE64_JSON: 'Base64 JSON',
HEX: 'Hex',
BINARY: 'Binary',
GZIP: 'GZip',
GZIP_JSON: 'GZip JSON',
DEFLATE: 'Deflate',
}

View File

@ -525,14 +525,15 @@ const useConnectionStore = defineStore('connections', {
* @param {string} server
* @param {number} db
* @param {string} [key] when key is null or blank, update tab to display normal content (blank content or server status)
* @param {string} [viewType]
*/
async loadKeyValue(server, db, key) {
async loadKeyValue(server, db, key, viewType) {
try {
const tab = useTabStore()
if (!isEmpty(key)) {
const { data, success, msg } = await GetKeyValue(server, db, key)
const { data, success, msg } = await GetKeyValue(server, db, key, viewType)
if (success) {
const { type, ttl, value, size } = data
const { type, ttl, value, size, viewAs } = data
tab.upsertTab({
server,
db,
@ -541,11 +542,16 @@ const useConnectionStore = defineStore('connections', {
key,
value,
size,
viewAs,
})
return
} else {
if (!isEmpty(msg)) {
$message.error('load key fail: ' + msg)
}
// its danger to delete "non-exists" key, just remove from tree view
await this.deleteKey(server, db, key, true)
// TODO: show key not found page?
}
}
@ -852,11 +858,12 @@ const useConnectionStore = defineStore('connections', {
* @param {string} keyType
* @param {any} value
* @param {number} ttl
* @param {string} [viewAs]
* @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>}
*/
async setKey(connName, db, key, keyType, value, ttl) {
async setKey(connName, db, key, keyType, value, ttl, viewAs) {
try {
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl)
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl, viewAs)
if (success) {
// update tree view data
const { newKey = 0 } = this._addKeyNodes(connName, db, [key], true)

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { lang } from '@/langs/index.js'
import { camelCase, clone, find, get, isEmpty, isObject, map, set, snakeCase, split } from 'lodash'
import { clone, find, get, isEmpty, map, pick, set, split } from 'lodash'
import {
CheckForUpdate,
GetFontList,
@ -38,7 +38,7 @@ const usePreferencesStore = defineStore('preferences', {
},
general: {
theme: 'auto',
language: 'en',
language: 'auto',
font: '',
fontSize: 14,
useSysProxy: false,
@ -165,8 +165,7 @@ const usePreferencesStore = defineStore('preferences', {
actions: {
_applyPreferences(data) {
for (const key in data) {
const keys = map(split(key, '.'), camelCase)
set(this, keys, data[key])
set(this, key, data[key])
}
},
@ -203,30 +202,13 @@ const usePreferencesStore = defineStore('preferences', {
* @returns {Promise<boolean>}
*/
async savePreferences() {
const obj2Map = (prefix, obj) => {
const result = {}
for (const key in obj) {
if (isObject(obj[key])) {
const subResult = obj2Map(`${prefix}.${snakeCase(key)}`, obj[key])
Object.assign(result, subResult)
} else {
result[`${prefix}.${snakeCase(key)}`] = obj[key]
}
}
return result
}
const pf = Object.assign(
{},
obj2Map('behavior', this.behavior),
obj2Map('general', this.general),
obj2Map('editor', this.editor),
)
const pf = pick(this, ['behavior', 'general', 'editor'])
const { success, msg } = await SetPreferences(pf)
return success === true
},
/**
* reset to last loaded preferences
* reset to last-loaded preferences
* @returns {Promise<void>}
*/
async resetToLastPreferences() {

View File

@ -85,8 +85,9 @@ const useTabStore = defineStore('tab', {
* @param {string} [key]
* @param {number} [size]
* @param {*} [value]
* @param {string} [viewAs]
*/
upsertTab({ server, db, type, ttl, key, size, value }) {
upsertTab({ server, db, type, ttl, key, size, value, viewAs }) {
let tabIndex = findIndex(this.tabList, { name: server })
if (tabIndex === -1) {
this.tabList.push({
@ -98,6 +99,7 @@ const useTabStore = defineStore('tab', {
key,
size,
value,
viewAs,
})
tabIndex = this.tabList.length - 1
}
@ -112,6 +114,7 @@ const useTabStore = defineStore('tab', {
tab.key = key
tab.size = size
tab.value = value
tab.viewAs = viewAs
this._setActivatedIndex(tabIndex, true)
// this.activatedTab = tab.name
},

View File

@ -1,25 +0,0 @@
import { endsWith, isEmpty, size, startsWith } from 'lodash'
export const IsRedisKey = (str, separator) => {
if (isEmpty(separator)) {
separator = ':'
}
}
/**
* check string is json
* @param str
* @returns {boolean}
* @constructor
*/
export const IsJson = (str) => {
if (size(str) >= 2) {
if (startsWith(str, '{') && endsWith(str, '}')) {
return true
}
if (startsWith(str, '[') && endsWith(str, ']')) {
return true
}
}
return false
}

View File

@ -1,70 +0,0 @@
import { map, padStart } from 'lodash'
/**
* convert string to json
* @param str
* @return {string}
*/
export const toJsonText = (str) => {
try {
const jsonObj = JSON.parse(str)
return JSON.stringify(jsonObj, null, 2)
} catch (e) {
return str
}
}
/**
* convert string from base64
* @param str
* @return {string}
*/
export const fromBase64 = (str) => {
try {
return atob(str)
} catch (e) {
return str
}
}
/**
* convert string from base64 to json
* @param str
* @return {string}
*/
export const fromBase64Json = (str) => {
try {
const text = atob(str)
const jsonObj = JSON.parse(text)
return JSON.stringify(jsonObj, null, 2)
} catch (e) {
return str
}
}
/**
* convert string to hex string
* @param str
* @return {string}
*/
export const toHex = (str) => {
const hexArr = map(str, (char) => {
const charCode = char.charCodeAt(0)
return charCode.toString(16)
})
return hexArr.join(' ')
}
/**
* convert string to binary string
* @param str
* @return {string}
*/
export const toBinary = (str) => {
const codeUnits = map(str, (char) => {
let code = char.charCodeAt(0).toString(2)
code = padStart(code, 8, '0')
return code
})
return codeUnits.join(' ')
}

10
main.go
View File

@ -57,6 +57,16 @@ func main() {
app.startup(ctx)
connSvc.Start(ctx)
},
//OnBeforeClose: func(ctx context.Context) (prevent bool) {
// // save current window size
// width, height := runtime2.WindowGetSize(ctx)
// if width > 0 && height > 0 {
// if w, h := prefSvc.GetWindowSize(); w != width || h != height {
// prefSvc.SaveWindowSize(width, height)
// }
// }
// return false
//},
OnShutdown: func(ctx context.Context) {
// save current window size
width, height := runtime2.WindowGetSize(ctx)