feat: split "view as" to "view type" and "decode type" #60

feat: add footer to the value content page
This commit is contained in:
tiny-craft 2023-10-29 23:05:34 +08:00
parent d0af3deaeb
commit 30866d33f0
22 changed files with 544 additions and 256 deletions

View File

@ -791,7 +791,7 @@ func (c *connectionService) LoadAllKeys(connName string, db int, match, keyType
} }
// GetKeyValue get value by key // GetKeyValue get value by key
func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs string) (resp types.JSResp) { func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs, decodeType string) (resp types.JSResp) {
item, err := c.getRedisClient(connName, db) item, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -831,7 +831,7 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
case "string": case "string":
var str string var str string
str, err = client.Get(ctx, key).Result() str, err = client.Get(ctx, key).Result()
value, viewAs = strutil.ConvertTo(str, viewAs) value, decodeType, viewAs = strutil.ConvertTo(str, decodeType, viewAs)
size, _ = client.StrLen(ctx, key).Result() size, _ = client.StrLen(ctx, key).Result()
case "list": case "list":
value, err = client.LRange(ctx, key, 0, -1).Result() value, err = client.LRange(ctx, key, 0, -1).Result()
@ -928,13 +928,14 @@ func (c *connectionService) GetKeyValue(connName string, db int, k any, viewAs s
"value": value, "value": value,
"size": size, "size": size,
"viewAs": viewAs, "viewAs": viewAs,
"decode": decodeType,
} }
return return
} }
// SetKeyValue set value by key // SetKeyValue set value by key
// @param ttl <= 0 means keep current ttl // @param ttl <= 0 means keep current ttl
func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType string, value any, ttl int64, viewAs string) (resp types.JSResp) { func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType string, value any, ttl int64, viewAs, decode string) (resp types.JSResp) {
item, err := c.getRedisClient(connName, db) item, err := c.getRedisClient(connName, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -958,7 +959,7 @@ func (c *connectionService) SetKeyValue(connName string, db int, k any, keyType
return return
} else { } else {
var saveStr string var saveStr string
if saveStr, err = strutil.SaveAs(str, viewAs); err != nil { if saveStr, err = strutil.SaveAs(str, viewAs, decode); err != nil {
resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error()) resp.Msg = fmt.Sprintf(`save to "%s" type fail: %s`, viewAs, err.Error())
return return
} }

View File

@ -1,14 +1,12 @@
package types package types
const PLAIN_TEXT = "Plain Text" const VIEWAS_PLAIN_TEXT = "Plain Text"
const JSON = "JSON" const VIEWAS_JSON = "JSON"
const BASE64_TEXT = "Base64 Text" const VIEWAS_HEX = "Hex"
const BASE64_JSON = "Base64 JSON" const VIEWAS_BINARY = "Binary"
const HEX = "Hex"
const BINARY = "Binary" const DECODE_NONE = "None"
const GZIP = "GZip" const DECODE_BASE64 = "Base64"
const GZIP_JSON = "GZip JSON" const DECODE_GZIP = "GZip"
const DEFLATE = "Deflate" const DECODE_DEFLATE = "Deflate"
const DEFLATE_JSON = "Deflate JSON" const DECODE_BROTLI = "Brotli"
const BROTLI = "Brotli"
const BROTLI_JSON = "Brotli JSON"

View File

@ -18,160 +18,171 @@ import (
) )
// ConvertTo convert string to specified type // ConvertTo convert string to specified type
// @param targetType empty string indicates automatic detection of the string type // @param decodeType empty string indicates automatic detection
func ConvertTo(str, targetType string) (value, resultType string) { // @param formatType empty string indicates automatic detection
func ConvertTo(str, decodeType, formatType string) (value, resultDecode, resultFormat string) {
if len(str) <= 0 { if len(str) <= 0 {
// empty content // empty content
if len(targetType) <= 0 { if len(formatType) <= 0 {
resultType = types.PLAIN_TEXT resultFormat = types.VIEWAS_PLAIN_TEXT
} else { } else {
resultType = targetType resultFormat = formatType
}
if len(decodeType) <= 0 {
resultDecode = types.DECODE_NONE
} else {
resultDecode = decodeType
} }
return return
} }
switch targetType { // decode first
case types.PLAIN_TEXT: value, resultDecode = decodeWith(str, decodeType)
// then format content
value, resultFormat = viewAs(value, formatType)
return
}
func decodeWith(str, decodeType string) (value, resultDecode string) {
if len(decodeType) > 0 {
switch decodeType {
case types.DECODE_NONE:
value = str value = str
resultType = targetType resultDecode = decodeType
return return
case types.JSON: case types.DECODE_BASE64:
value, _ = decodeJson(str)
resultType = targetType
return
case types.BASE64_TEXT, types.BASE64_JSON:
if base64Str, ok := decodeBase64(str); ok { if base64Str, ok := decodeBase64(str); ok {
if targetType == types.BASE64_JSON {
value, _ = decodeJson(base64Str)
} else {
value = base64Str value = base64Str
}
} else { } else {
value = str value = str
} }
resultType = targetType resultDecode = decodeType
return return
case types.HEX: case types.DECODE_GZIP:
if gzipStr, ok := decodeGZip(str); ok {
value = gzipStr
} else {
value = str
}
resultDecode = decodeType
return
case types.DECODE_DEFLATE:
if gzipStr, ok := decodeDeflate(str); ok {
value = gzipStr
} else {
value = str
}
resultDecode = decodeType
return
case types.DECODE_BROTLI:
if gzipStr, ok := decodeBrotli(str); ok {
value = gzipStr
} else {
value = str
}
resultDecode = decodeType
return
}
}
return autoDecode(str)
}
// attempt try possible decode method
// if no decode is possible, it will return the origin string value and "none" decode type
func autoDecode(str string) (value, resultDecode string) {
if len(str) > 0 {
var ok bool
if value, ok = decodeBase64(str); ok {
resultDecode = types.DECODE_BASE64
return
}
if value, ok = decodeGZip(str); ok {
resultDecode = types.DECODE_GZIP
return
}
if value, ok = decodeDeflate(str); ok {
resultDecode = types.DECODE_DEFLATE
return
}
if value, ok = decodeBrotli(str); ok {
resultDecode = types.DECODE_BROTLI
return
}
}
value = str
resultDecode = types.DECODE_NONE
return
}
func viewAs(str, formatType string) (value, resultFormat string) {
if len(formatType) > 0 {
switch formatType {
case types.VIEWAS_PLAIN_TEXT:
value = str
resultFormat = formatType
return
case types.VIEWAS_JSON:
if jsonStr, ok := decodeJson(str); ok {
value = jsonStr
} else {
value = str
}
resultFormat = formatType
return
case types.VIEWAS_HEX:
if hexStr, ok := decodeToHex(str); ok { if hexStr, ok := decodeToHex(str); ok {
value = hexStr value = hexStr
} else { } else {
value = str value = str
} }
resultType = targetType resultFormat = formatType
return return
case types.BINARY: case types.VIEWAS_BINARY:
if binStr, ok := decodeBinary(str); ok { if binStr, ok := decodeBinary(str); ok {
value = binStr value = binStr
} else { } else {
value = str value = str
} }
resultType = targetType resultFormat = formatType
return
case types.GZIP, types.GZIP_JSON:
if gzipStr, ok := decodeGZip(str); ok {
if targetType == types.GZIP_JSON {
value, _ = decodeJson(gzipStr)
} else {
value = gzipStr
}
} else {
value = str
}
resultType = targetType
return
case types.DEFLATE, types.DEFLATE_JSON:
if deflateStr, ok := decodeDeflate(str); ok {
if targetType == types.DEFLATE_JSON {
value, _ = decodeJson(deflateStr)
} else {
value = deflateStr
}
} else {
value = str
}
resultType = targetType
return
case types.BROTLI, types.BROTLI_JSON:
if brotliStr, ok := decodeBrotli(str); ok {
if targetType == types.BROTLI_JSON {
value, _ = decodeJson(brotliStr)
} else {
value = brotliStr
}
} else {
value = str
}
resultType = targetType
return return
} }
}
// type isn't specified or unknown, try to automatically detect and return converted value return autoViewAs(str)
return autoToType(str)
} }
// attempt automatic convert to possible types // attempt automatic convert to possible types
// if no conversion is possible, it will return the origin string value and "plain text" type // if no conversion is possible, it will return the origin string value and "plain text" type
func autoToType(str string) (value, resultType string) { func autoViewAs(str string) (value, resultFormat string) {
if len(str) > 0 { if len(str) > 0 {
var ok bool var ok bool
if value, ok = decodeJson(str); ok { if value, ok = decodeJson(str); ok {
resultType = types.JSON resultFormat = types.VIEWAS_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 {
if value, ok = decodeJson(value); ok {
resultType = types.GZIP_JSON
return
}
resultType = types.GZIP
return
}
if value, ok = decodeDeflate(str); ok {
if value, ok = decodeJson(value); ok {
resultType = types.DEFLATE_JSON
return
}
resultType = types.DEFLATE
return
}
if value, ok = decodeBrotli(str); ok {
if value, ok = decodeJson(value); ok {
resultType = types.BROTLI_JSON
return
}
resultType = types.BROTLI
return return
} }
if containsBinary(str) { if containsBinary(str) {
if value, ok = decodeToHex(str); ok { if value, ok = decodeToHex(str); ok {
resultType = types.HEX resultFormat = types.VIEWAS_HEX
return return
} }
} }
} }
value = str value = str
resultType = types.PLAIN_TEXT resultFormat = types.VIEWAS_PLAIN_TEXT
return return
} }
@ -241,65 +252,65 @@ func decodeBrotli(str string) (string, bool) {
return str, false return str, false
} }
func SaveAs(str, targetType string) (value string, err error) { func SaveAs(str, viewType, decodeType string) (value string, err error) {
switch targetType { value = str
case types.PLAIN_TEXT: switch viewType {
return str, nil case types.VIEWAS_JSON:
case types.BASE64_TEXT:
base64Str, _ := encodeBase64(str)
return base64Str, nil
case types.HEX:
hexStr, _ := encodeHex(str)
return hexStr, nil
case types.BINARY:
binStr, _ := encodeBinary(str)
return binStr, nil
case types.JSON, types.BASE64_JSON, types.GZIP_JSON, types.DEFLATE_JSON, types.BROTLI_JSON:
if jsonStr, ok := encodeJson(str); ok { if jsonStr, ok := encodeJson(str); ok {
switch targetType { value = jsonStr
case types.BASE64_JSON:
base64Str, _ := encodeBase64(jsonStr)
return base64Str, nil
case types.GZIP_JSON:
gzipStr, _ := encodeGZip(jsonStr)
return gzipStr, nil
case types.DEFLATE_JSON:
deflateStr, _ := encodeDeflate(jsonStr)
return deflateStr, nil
case types.BROTLI_JSON:
brotliStr, _ := encodeBrotli(jsonStr)
return brotliStr, nil
default:
return jsonStr, nil
}
} else { } else {
return str, errors.New("invalid json") err = errors.New("invalid json data")
return
} }
case types.GZIP: case types.VIEWAS_HEX:
if hexStr, ok := encodeHex(str); ok {
value = hexStr
} else {
err = errors.New("invalid hex data")
return
}
case types.VIEWAS_BINARY:
if binStr, ok := encodeBinary(str); ok {
value = binStr
} else {
err = errors.New("invalid binary data")
return
}
}
switch decodeType {
case types.DECODE_NONE:
return
case types.DECODE_BASE64:
value, _ = encodeBase64(value)
return
case types.DECODE_GZIP:
if gzipStr, ok := encodeGZip(str); ok { if gzipStr, ok := encodeGZip(str); ok {
return gzipStr, nil value = gzipStr
} else { } else {
return str, errors.New("fail to build gzip data") err = errors.New("fail to build gzip")
} }
return
case types.DEFLATE: case types.DECODE_DEFLATE:
if deflateStr, ok := encodeDeflate(str); ok { if deflateStr, ok := encodeDeflate(str); ok {
return deflateStr, nil value = deflateStr
} else { } else {
return str, errors.New("fail to build deflate data") err = errors.New("fail to build deflate")
} }
return
case types.BROTLI: case types.DECODE_BROTLI:
if brotliStr, ok := encodeBrotli(str); ok { if brotliStr, ok := encodeBrotli(str); ok {
return brotliStr, nil value = brotliStr
} else { } else {
return str, errors.New("fail to build brotli data") err = errors.New("fail to build brotli")
} }
return
} }
return str, errors.New("fail to save with unknown error") return str, errors.New("fail to save with unknown error")
} }

View File

@ -107,8 +107,8 @@ onMounted(async () => {
<!-- app content--> <!-- app content-->
<n-spin <n-spin
:show="props.loading" :show="props.loading"
:theme-overrides="{ opacitySpinning: 0 }" :style="{ backgroundColor: themeVars.bodyColor }"
:style="{ backgroundColor: themeVars.bodyColor }"> :theme-overrides="{ opacitySpinning: 0 }">
<div id="app-content-wrapper" :style="wrapperStyle" class="flex-box-v"> <div id="app-content-wrapper" :style="wrapperStyle" class="flex-box-v">
<!-- title bar --> <!-- title bar -->
<div <div

View File

@ -34,11 +34,11 @@ const hasTooltip = computed(() => {
<n-tooltip v-if="hasTooltip" :show-arrow="false"> <n-tooltip v-if="hasTooltip" :show-arrow="false">
<template #trigger> <template #trigger>
<n-button <n-button
:color="props.color"
:disabled="disabled" :disabled="disabled"
:focusable="false" :focusable="false"
:loading="loading" :loading="loading"
:text="!border" :text="!border"
:color="props.color"
@click.prevent="emit('click')"> @click.prevent="emit('click')">
<template #icon> <template #icon>
<n-icon :color="props.color || 'currentColor'" :size="props.size"> <n-icon :color="props.color || 'currentColor'" :size="props.size">
@ -51,11 +51,11 @@ const hasTooltip = computed(() => {
</n-tooltip> </n-tooltip>
<n-button <n-button
v-else v-else
:color="props.color"
:disabled="disabled" :disabled="disabled"
:focusable="false" :focusable="false"
:loading="loading" :loading="loading"
:text="!border" :text="!border"
:color="props.color"
@click.prevent="emit('click')"> @click.prevent="emit('click')">
<template #icon> <template #icon>
<n-icon :color="props.color || 'currentColor'" :size="props.size"> <n-icon :color="props.color || 'currentColor'" :size="props.size">

View File

@ -51,6 +51,7 @@ const tabContent = computed(() => {
value: tab.value, value: tab.value,
size: tab.size || 0, size: tab.size || 0,
viewAs: tab.viewAs, viewAs: tab.viewAs,
decode: tab.decode,
} }
}) })
@ -127,6 +128,7 @@ watch(
<content-value-wrapper <content-value-wrapper
:blank="isBlankValue" :blank="isBlankValue"
:db="tabContent.db" :db="tabContent.db"
:decode="tabContent.decode"
:key-code="tabContent.keyCode" :key-code="tabContent.keyCode"
:key-path="tabContent.keyPath" :key-path="tabContent.keyPath"
:name="tabContent.name" :name="tabContent.name"

View File

@ -128,7 +128,7 @@ const onDeleteKey = () => {
</n-tooltip> </n-tooltip>
<icon-button :icon="Edit" border size="18" t-tooltip="interface.rename_key" @click="onRenameKey" /> <icon-button :icon="Edit" border size="18" t-tooltip="interface.rename_key" @click="onRenameKey" />
</n-button-group> </n-button-group>
<n-tooltip> <n-tooltip :show-arrow="false">
<template #trigger> <template #trigger>
<n-button :focusable="false" @click="onDeleteKey"> <n-button :focusable="false" @click="onDeleteKey">
<template #icon> <template #icon>

View File

@ -1,17 +1,20 @@
<script setup> <script setup>
import { computed, onMounted, ref, watch } from 'vue' import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ContentToolbar from './ContentToolbar.vue' import ContentToolbar from './ContentToolbar.vue'
import Copy from '@/components/icons/Copy.vue' import Copy from '@/components/icons/Copy.vue'
import Save from '@/components/icons/Save.vue' import Save from '@/components/icons/Save.vue'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import { types } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import Close from '@/components/icons/Close.vue' import Close from '@/components/icons/Close.vue'
import Edit from '@/components/icons/Edit.vue'
import { types as redisTypes } from '@/consts/support_redis_type.js' import { types as redisTypes } from '@/consts/support_redis_type.js'
import { ClipboardSetText } from 'wailsjs/runtime/runtime.js' import { ClipboardSetText } from 'wailsjs/runtime/runtime.js'
import { isEmpty, map, toLower } from 'lodash' import { isEmpty, toLower } from 'lodash'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import DropdownSelector from '@/components/content_value/DropdownSelector.vue'
import Code from '@/components/icons/Code.vue'
import Conversion from '@/components/icons/Conversion.vue'
import EditFile from '@/components/icons/EditFile.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -32,7 +35,11 @@ const props = defineProps({
size: Number, size: Number,
viewAs: { viewAs: {
type: String, type: String,
default: types.PLAIN_TEXT, default: formatTypes.PLAIN_TEXT,
},
decode: {
type: String,
default: decodeTypes.NONE,
}, },
}) })
@ -44,43 +51,20 @@ const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
}) })
const viewOption = computed(() => // const viewOption = computed(() =>
map(types, (t) => { // map(types, (t) => {
return { // return {
value: t, // value: t,
label: t, // label: t,
} // key: t,
}), // }
) // }),
// 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
// }
}
onMounted(() => {
autoDetectFormat()
})
watch(
() => props.value,
(value) => {
autoDetectFormat()
},
)
const keyType = redisTypes.STRING const keyType = redisTypes.STRING
const viewLanguage = computed(() => { const viewLanguage = computed(() => {
switch (props.viewAs) { switch (props.viewAs) {
case types.JSON: case formatTypes.JSON:
case types.BASE64_JSON:
case types.GZIP_JSON:
case types.DEFLATE_JSON:
case types.BROTLI_JSON:
return 'json' return 'json'
default: default:
return 'plaintext' return 'plaintext'
@ -88,7 +72,11 @@ const viewLanguage = computed(() => {
}) })
const onViewTypeUpdate = (viewType) => { const onViewTypeUpdate = (viewType) => {
connectionStore.loadKeyValue(props.name, props.db, keyName.value, viewType) connectionStore.loadKeyValue(props.name, props.db, keyName.value, viewType, props.decode)
}
const onDecodeTypeUpdate = (decodeType) => {
connectionStore.loadKeyValue(props.name, props.db, keyName.value, props.viewAs, decodeType)
} }
/** /**
@ -133,6 +121,7 @@ const onSaveValue = async () => {
editValue.value, editValue.value,
-1, -1,
props.viewAs, props.viewAs,
props.decode,
) )
if (success) { if (success) {
await connectionStore.loadKeyValue(props.name, props.db, keyName.value) await connectionStore.loadKeyValue(props.name, props.db, keyName.value)
@ -157,15 +146,9 @@ const onSaveValue = async () => {
:key-path="keyPath" :key-path="keyPath"
:key-type="keyType" :key-type="keyType"
:server="props.name" :server="props.name"
:ttl="ttl" /> :ttl="ttl"
<div class="tb2 flex-box-h"> class="value-item-part" />
<n-text>{{ $t('interface.view_as') }}</n-text> <div class="tb2 value-item-part flex-box-h">
<n-select
:options="viewOption"
:value="props.viewAs"
filterable
style="width: 160px"
@update:value="onViewTypeUpdate" />
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group v-if="!inEdit"> <n-button-group v-if="!inEdit">
<n-button :focusable="false" @click="onCopyValue"> <n-button :focusable="false" @click="onCopyValue">
@ -176,7 +159,7 @@ const onSaveValue = async () => {
</n-button> </n-button>
<n-button :focusable="false" plain @click="onEditValue"> <n-button :focusable="false" plain @click="onEditValue">
<template #icon> <template #icon>
<n-icon :component="Edit" size="18" /> <n-icon :component="EditFile" size="18" />
</template> </template>
{{ $t('interface.edit_value') }} {{ $t('interface.edit_value') }}
</n-button> </n-button>
@ -196,7 +179,7 @@ const onSaveValue = async () => {
</n-button> </n-button>
</n-button-group> </n-button-group>
</div> </div>
<div class="value-wrapper flex-item-expand flex-box-v"> <div class="value-wrapper value-item-part flex-item-expand flex-box-v">
<n-scrollbar v-if="!inEdit" class="flex-item-expand"> <n-scrollbar v-if="!inEdit" class="flex-item-expand">
<n-code :code="props.value" :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-scrollbar>
@ -208,6 +191,24 @@ const onSaveValue = async () => {
class="flex-item-expand" class="flex-item-expand"
type="textarea" /> type="textarea" />
</div> </div>
<div class="value-footer flex-box-h">
<div class="flex-item-expand"></div>
<dropdown-selector
:icon="Code"
:options="formatTypes"
:tooltip="$t('interface.view_as')"
:value="props.viewAs"
@update:value="onViewTypeUpdate" />
<n-divider vertical />
<dropdown-selector
:icon="Conversion"
:options="decodeTypes"
:tooltip="$t('interface.decode_with')"
:value="props.decode"
@update:value="onDecodeTypeUpdate" />
</div>
</div> </div>
</template> </template>
@ -217,4 +218,9 @@ const onSaveValue = async () => {
border-top: v-bind('themeVars.borderColor') 1px solid; border-top: v-bind('themeVars.borderColor') 1px solid;
padding: 5px; padding: 5px;
} }
.value-footer {
border-top: v-bind('themeVars.borderColor') 1px solid;
background-color: v-bind('themeVars.bodyColor');
}
</style> </style>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { types } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import { types as redisTypes } from '@/consts/support_redis_type.js' import { types as redisTypes } from '@/consts/support_redis_type.js'
import ContentValueString from '@/components/content_value/ContentValueString.vue' import ContentValueString from '@/components/content_value/ContentValueString.vue'
import ContentValueHash from '@/components/content_value/ContentValueHash.vue' import ContentValueHash from '@/components/content_value/ContentValueHash.vue'
@ -31,7 +31,11 @@ const props = defineProps({
size: Number, size: Number,
viewAs: { viewAs: {
type: String, type: String,
default: types.PLAIN_TEXT, default: formatTypes.PLAIN_TEXT,
},
decode: {
type: String,
default: decodeTypes.NONE,
}, },
}) })
@ -63,6 +67,7 @@ const onReloadKey = async () => {
<component <component
:is="valueComponents[props.type]" :is="valueComponents[props.type]"
:db="props.db" :db="props.db"
:decode="props.decode"
:key-code="props.keyCode" :key-code="props.keyCode"
:key-path="props.keyPath" :key-path="props.keyPath"
:name="props.name" :name="props.name"

View File

@ -0,0 +1,94 @@
<script setup>
import { computed, h, ref } from 'vue'
import { map } from 'lodash'
import { NIcon, NText } from 'naive-ui'
const props = defineProps({
value: {
type: String,
value: '',
},
options: {
type: Object,
value: {},
},
tooltip: {
type: String,
},
icon: [String, Object],
})
const emit = defineEmits(['update:value'])
const renderHeader = () => {
return h('div', { class: 'type-selector-header' }, [h(NText, null, () => props.tooltip)])
}
const renderLabel = (option) => {
return h('div', { class: 'type-selector-item' }, option.label)
}
const dropdownOption = computed(() => {
const options = [
{
key: 'header',
type: 'render',
render: renderHeader,
},
{
key: 'header-divider',
type: 'divider',
},
]
return [
...options,
...map(props.options, (t) => {
return {
key: t,
label: t,
}
}),
]
})
const onDropdownSelect = (key) => {
emit('update:value', key)
}
const buttonText = computed(() => {
return props.value || get(dropdownOption.value, [1, 'label'], 'None')
})
const showDropdown = ref(false)
const onDropdownShow = (show) => {
showDropdown.value = show === true
}
</script>
<template>
<n-dropdown
:options="dropdownOption"
:render-label="renderLabel"
:show-arrow="true"
:title="props.tooltip"
:value="props.value"
trigger="click"
@select="onDropdownSelect"
@update:show="onDropdownShow">
<n-tooltip :disabled="showDropdown" :show-arrow="false">
{{ props.tooltip }}
<template #trigger>
<n-button :focusable="false" quaternary>
<template #icon>
<n-icon>
<component :is="icon" />
</n-icon>
</template>
{{ buttonText }}
</n-button>
</template>
</n-tooltip>
</n-dropdown>
</template>
<style lang="scss" scoped></style>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed, h, reactive, ref, watch } from 'vue' import { computed, h, reactive, ref, watch } from 'vue'
import { types, typesColor } from '@/consts/support_redis_type.js' import { types, typesColor } from '@/consts/support_redis_type.js'
import { types as viewTypes } from '@/consts/value_view_type.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js'
import useDialog from 'stores/dialog' import useDialog from 'stores/dialog'
import { isEmpty, keys, map } from 'lodash' import { isEmpty, keys, map } from 'lodash'
import NewStringValue from '@/components/new_value/NewStringValue.vue' import NewStringValue from '@/components/new_value/NewStringValue.vue'
@ -124,7 +124,8 @@ const onAdd = async () => {
type, type,
value, value,
ttl, ttl,
viewTypes.PLAIN_TEXT, formatTypes.PLAIN_TEXT,
decodeTypes.NONE,
) )
if (success) { if (success) {
// select current key // select current key

View File

@ -0,0 +1,39 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M40 23V14L31 4H10C8.89543 4 8 4.89543 8 6V42C8 43.1046 8.89543 44 10 44H22"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M37 31L42 36L37 41"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M31 31L26 36L31 41"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M30 4V14H40"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,51 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M40 23V14L31 4H10C8.89543 4 8 4.89543 8 6V42C8 43.1046 8.89543 44 10 44H22"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M27 33H41"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M27 39H41"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M41 33L36 28"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M32 44L27 39"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M30 4V14H40"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -12,7 +12,7 @@ const props = defineProps({
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M7 42H43" d="M7 42H43"
stroke="currentColorr" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
<path <path

View File

@ -0,0 +1,34 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path
:stroke-width="props.strokeWidth"
d="M40 23V14L31 4H10C8.89543 4 8 4.89543 8 6V42C8 43.1046 8.89543 44 10 44H22"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M32 44L42 34L38 30L28 40V44H32Z"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
<path
:stroke-width="props.strokeWidth"
d="M30 4V14H40"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -1,18 +1,24 @@
/** /**
* string view mode * string format types
* @enum {string} * @enum {string}
*/ */
export const types = { export const formatTypes = {
PLAIN_TEXT: 'Plain Text', PLAIN_TEXT: 'Plain Text',
JSON: 'JSON', JSON: 'JSON',
BASE64_TEXT: 'Base64 Text',
BASE64_JSON: 'Base64 JSON',
HEX: 'Hex', HEX: 'Hex',
BINARY: 'Binary', BINARY: 'Binary',
GZIP: 'GZip', }
GZIP_JSON: 'GZip JSON',
DEFLATE: 'Deflate', /**
DEFLATE_JSON: 'Deflate JSON', * string decode types
BROTLI: 'Brotli', * @enum {string}
BROTLI_JSON: 'Brotli JSON', */
export const decodeTypes = {
NONE: 'None',
BASE64: 'Base64',
GZIP: 'GZip',
DEFLATE: 'Deflate',
BROTLI: 'Brotli',
// PHP: 'PHP',
// Java: 'Java',
} }

View File

@ -76,6 +76,7 @@
"filter_field": "Filter Field", "filter_field": "Filter Field",
"filter_value": "Filter Value", "filter_value": "Filter Value",
"view_as": "View As", "view_as": "View As",
"decode_with": "Decode/Decompress Type",
"reload": "Reload", "reload": "Reload",
"open_connection": "Open Connection", "open_connection": "Open Connection",
"batch_delete": "Batch Delete", "batch_delete": "Batch Delete",

View File

@ -76,6 +76,7 @@
"filter_field": "筛选字段", "filter_field": "筛选字段",
"filter_value": "筛选值", "filter_value": "筛选值",
"view_as": "查看方式", "view_as": "查看方式",
"decode_with": "解码/解压方式",
"reload": "重新载入", "reload": "重新载入",
"open_connection": "打开连接", "open_connection": "打开连接",
"batch_delete": "批量删除键", "batch_delete": "批量删除键",

View File

@ -657,14 +657,15 @@ const useConnectionStore = defineStore('connections', {
* @param {number} db * @param {number} db
* @param {string|number[]} [key] when key is null or blank, update tab to display normal content (blank content or server status) * @param {string|number[]} [key] when key is null or blank, update tab to display normal content (blank content or server status)
* @param {string} [viewType] * @param {string} [viewType]
* @param {string} [decodeType]
*/ */
async loadKeyValue(server, db, key, viewType) { async loadKeyValue(server, db, key, viewType, decodeType) {
try { try {
const tab = useTabStore() const tab = useTabStore()
if (!isEmpty(key)) { if (!isEmpty(key)) {
const { data, success, msg } = await GetKeyValue(server, db, key, viewType) const { data, success, msg } = await GetKeyValue(server, db, key, viewType, decodeType)
if (success) { if (success) {
const { type, ttl, value, size, viewAs } = data const { type, ttl, value, size, viewAs, decode } = data
const k = decodeRedisKey(key) const k = decodeRedisKey(key)
const binaryKey = k !== key const binaryKey = k !== key
tab.upsertTab({ tab.upsertTab({
@ -678,6 +679,7 @@ const useConnectionStore = defineStore('connections', {
value, value,
size, size,
viewAs, viewAs,
decode,
}) })
return return
} else { } else {
@ -1092,11 +1094,12 @@ const useConnectionStore = defineStore('connections', {
* @param {any} value * @param {any} value
* @param {number} ttl * @param {number} ttl
* @param {string} [viewAs] * @param {string} [viewAs]
* @param {string} [decode]
* @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>} * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>}
*/ */
async setKey(connName, db, key, keyType, value, ttl, viewAs) { async setKey(connName, db, key, keyType, value, ttl, viewAs, decode) {
try { try {
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl, viewAs) const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl, viewAs, decode)
if (success) { if (success) {
// update tree view data // update tree view data
const { newKey = 0 } = this._addKeyNodes(connName, db, [key], true) const { newKey = 0 } = this._addKeyNodes(connName, db, [key], true)

View File

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

View File

@ -79,7 +79,8 @@ body {
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
gap: 5px; gap: 5px;
padding: 5px; padding-top: 5px;
//padding: 5px;
box-sizing: border-box; box-sizing: border-box;
.tb2 { .tb2 {
@ -97,6 +98,17 @@ body {
user-select: text; user-select: text;
height: 100%; height: 100%;
} }
.value-item-part {
padding: 0 5px;
}
.value-footer {
align-items: center;
gap: 0;
padding: 3px 10px 3px 10px;
min-height: 30px;
}
} }
.n-dynamic-input-item { .n-dynamic-input-item {
@ -113,14 +125,27 @@ body {
padding-right: 10px; padding-right: 10px;
} }
.type-selector-header {
height: 30px;
line-height: 30px;
font-size: 15px;
font-weight: bold;
text-align: center;
}
.type-selector-item {
min-width: 100px;
text-align: center;
}
.nav-pane-container { .nav-pane-container {
overflow: hidden; overflow: hidden;
.nav-pane-bottom { .nav-pane-bottom {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 3px 5px 5px 5px; padding: 3px 10px 3px 10px;
min-height: 35px; min-height: 30px;
//border-top: v-bind('themeVars.borderColor') 1px solid; //border-top: v-bind('themeVars.borderColor') 1px solid;
} }
} }

View File

@ -16,8 +16,15 @@ export const themeOverrides = {
scrollbarWidth: '8px', scrollbarWidth: '8px',
tabColor: '#FFFFFF', tabColor: '#FFFFFF',
}, },
Button: {
heightMedium: '32px',
},
Tag: { Tag: {
// borderRadius: '3px' // borderRadius: '3px'
heightLarge: '32px',
},
Input: {
heightMedium: '32px',
}, },
Tabs: { Tabs: {
tabGapSmallCard: '2px', tabGapSmallCard: '2px',