feat: adjusted the content pane to accommodate more information (add sub content tabs).

This commit is contained in:
tiny-craft 2023-10-23 19:45:04 +08:00
parent 6f5ea748f5
commit 5b4683a735
14 changed files with 445 additions and 91 deletions

View File

@ -1,16 +1,19 @@
<script setup> <script setup>
import { computed, onMounted, onUnmounted, reactive, watch } from 'vue' import { computed, onMounted, onUnmounted, reactive, watch } from 'vue'
import { types } from '@/consts/support_redis_type.js'
import ContentValueHash from '@/components/content_value/ContentValueHash.vue'
import ContentValueList from '@/components/content_value/ContentValueList.vue'
import ContentValueString from '@/components/content_value/ContentValueString.vue'
import ContentValueSet from '@/components/content_value/ContentValueSet.vue'
import ContentValueZset from '@/components/content_value/ContentValueZSet.vue'
import { get, isEmpty, keyBy, map, size, toUpper } from 'lodash' import { get, isEmpty, keyBy, map, size, toUpper } from 'lodash'
import useTabStore from 'stores/tab.js' import useTabStore from 'stores/tab.js'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import ContentServerStatus from '@/components/content_value/ContentServerStatus.vue' import ContentServerStatus from '@/components/content_value/ContentServerStatus.vue'
import ContentValueStream from '@/components/content_value/ContentValueStream.vue' import Status from '@/components/icons/Status.vue'
import { useThemeVars } from 'naive-ui'
import { BrowserTabType } from '@/consts/browser_tab_type.js'
import Terminal from '@/components/icons/Terminal.vue'
import Log from '@/components/icons/Log.vue'
import Detail from '@/components/icons/Detail.vue'
import ContentValueWrapper from '@/components/content_value/ContentValueWrapper.vue'
import ContentCli from '@/components/content_value/ContentCli.vue'
const themeVars = useThemeVars()
/** /**
* @typedef {Object} ServerStatusItem * @typedef {Object} ServerStatusItem
@ -112,15 +115,6 @@ onUnmounted(() => {
clearInterval(intervalId) clearInterval(intervalId)
}) })
const valueComponents = {
[types.STRING]: ContentValueString,
[types.HASH]: ContentValueHash,
[types.LIST]: ContentValueList,
[types.SET]: ContentValueSet,
[types.ZSET]: ContentValueZset,
[types.STREAM]: ContentValueStream,
}
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
const tabStore = useTabStore() const tabStore = useTabStore()
const tab = computed(() => const tab = computed(() =>
@ -162,6 +156,7 @@ const tabContent = computed(() => {
} }
return { return {
name: tab.name, name: tab.name,
subTab: tab.subTab,
type: toUpper(tab.type), type: toUpper(tab.type),
db: tab.db, db: tab.db,
keyPath: tab.key, keyPath: tab.key,
@ -177,7 +172,7 @@ const showServerStatus = computed(() => {
return tabContent.value == null || isEmpty(tabContent.value.keyPath) return tabContent.value == null || isEmpty(tabContent.value.keyPath)
}) })
const showNonexists = computed(() => { const isBlankValue = computed(() => {
return tabContent.value.value == null return tabContent.value.value == null
}) })
@ -192,38 +187,104 @@ const onReloadKey = async () => {
} }
await connectionStore.loadKeyValue(tab.name, tab.db, tab.key, tab.viewAs) await connectionStore.loadKeyValue(tab.name, tab.db, tab.key, tab.viewAs)
} }
const selectedSubTab = computed(() => {
const { subTab = 'status' } = tabStore.currentTab || {}
return subTab
})
const onSwitchSubTab = (name) => {
tabStore.switchSubTab(name)
}
</script> </script>
<template> <template>
<div class="content-container flex-box-v"> <div class="content-container flex-box-v">
<div v-if="showServerStatus" class="content-container flex-item-expand flex-box-v"> <n-tabs
<!-- select nothing or select server node, display server status --> :tabs-padding="5"
<content-server-status :theme-overrides="{
v-model:auto-refresh="currentServer.autoRefresh" tabGapSmallLine: '10px',
:auto-loading="currentServer.autoLoading" tabGapMediumLine: '10px',
:info="currentServer.info" tabGapLargeLine: '10px',
:loading="currentServer.loading" }"
:server="currentServer.name" :value="selectedSubTab"
@refresh="refreshInfo(currentServer.name, true)" /> class="content-sub-tab"
</div> default-value="status"
<div v-else-if="showNonexists" class="content-container flex-item-expand flex-box-v"> pane-class="content-sub-tab-pane"
<n-empty :description="$t('interface.nonexist_tab_content')" class="empty-content"> placement="top"
<template #extra> tab-style="padding-left: 10px; padding-right: 10px;"
<n-button :focusable="false" @click="onReloadKey">{{ $t('interface.reload') }}</n-button> type="line"
@update:value="onSwitchSubTab">
<!-- server status pane -->
<n-tab-pane :name="BrowserTabType.Status.toString()">
<template #tab>
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="16">
<status :inverse="selectedSubTab === BrowserTabType.Status.toString()" stroke-width="4" />
</n-icon>
<span>{{ $t('interface.sub_tab.status') }}</span>
</n-space>
</template> </template>
</n-empty> <content-server-status
</div> v-model:auto-refresh="currentServer.autoRefresh"
<component :auto-loading="currentServer.autoLoading"
:is="valueComponents[tabContent.type]" :info="currentServer.info"
v-else :loading="currentServer.loading"
:db="tabContent.db" :server="currentServer.name"
:key-code="tabContent.keyCode" @refresh="refreshInfo(currentServer.name, true)" />
:key-path="tabContent.keyPath" </n-tab-pane>
:name="tabContent.name"
:size="tabContent.size" <!-- key detail pane -->
:ttl="tabContent.ttl" <n-tab-pane :name="BrowserTabType.KeyDetail.toString()">
:value="tabContent.value" <template #tab>
:view-as="tabContent.viewAs" /> <n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="16">
<detail
:inverse="selectedSubTab === BrowserTabType.KeyDetail.toString()"
fill-color="none" />
</n-icon>
<span>{{ $t('interface.sub_tab.key_detail') }}</span>
</n-space>
</template>
<content-value-wrapper
:blank="isBlankValue"
:type="tabContent.type"
:db="tabContent.db"
:key-code="tabContent.keyCode"
:key-path="tabContent.keyPath"
:name="tabContent.name"
:size="tabContent.size"
:ttl="tabContent.ttl"
:value="tabContent.value"
:view-as="tabContent.viewAs"
@reload="onReloadKey" />
</n-tab-pane>
<!-- cli pane -->
<n-tab-pane :name="BrowserTabType.Cli.toString()">
<template #tab>
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="16">
<terminal :inverse="selectedSubTab === BrowserTabType.Cli.toString()" />
</n-icon>
<span>{{ $t('interface.sub_tab.cli') }}</span>
</n-space>
</template>
<content-cli />
</n-tab-pane>
<!-- slow log pane -->
<n-tab-pane :name="BrowserTabType.SlowLog.toString()">
<template #tab>
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="16">
<log :inverse="selectedSubTab === BrowserTabType.SlowLog.toString()" />
</n-icon>
<span>{{ $t('interface.sub_tab.slow_log') }}</span>
</n-space>
</template>
</n-tab-pane>
</n-tabs>
</div> </div>
</template> </template>
@ -231,27 +292,24 @@ const onReloadKey = async () => {
@import '@/styles/content'; @import '@/styles/content';
.content-container { .content-container {
padding: 5px; //padding: 5px 5px 0;
//padding-top: 0;
box-sizing: border-box; box-sizing: border-box;
background-color: v-bind('themeVars.tabColor');
}
</style>
<style lang="scss">
.content-sub-tab {
margin-bottom: 5px;
background-color: v-bind('themeVars.bodyColor');
height: 100%;
} }
//.tab-item { .content-sub-tab-pane {
// gap: 5px; padding: 0 !important;
// padding: 0 5px 0 10px; height: 100%;
// align-items: center; background-color: v-bind('themeVars.tabColor');
// max-width: 150px; overflow: hidden;
// }
// transition: all var(--transition-duration-fast) var(--transition-function-ease-in-out-bezier);
//
// &-label {
// font-size: 15px;
// text-align: center;
// }
//
// &-close {
// &:hover {
// background-color: rgb(176, 177, 182, 0.4);
// }
// }
//}
</style> </style>

View File

@ -83,7 +83,7 @@ const tab = computed(() =>
@dblclick.stop="() => {}"> @dblclick.stop="() => {}">
<n-space :size="5" :wrap-item="false" align="center" inline justify="center"> <n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="18"> <n-icon size="18">
<server stroke-width="4" :inverse="tabStore.activatedIndex === i" /> <server stroke-width="4" />
</n-icon> </n-icon>
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis> <n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
</n-space> </n-space>

View File

@ -0,0 +1,7 @@
<script setup></script>
<template>
<n-empty description="coming soon" class="empty-content"></n-empty>
</template>
<style scoped lang="scss"></style>

View File

@ -75,7 +75,7 @@ const infoFilter = ref('')
<template> <template>
<n-scrollbar ref="scrollRef"> <n-scrollbar ref="scrollRef">
<n-back-top :listen-to="scrollRef" /> <n-back-top :listen-to="scrollRef" />
<n-space vertical> <n-space vertical :wrap-item="false" :size="5" style="padding: 5px">
<n-card> <n-card>
<template #header> <template #header>
<n-space :wrap-item="false" align="center" inline size="small"> <n-space :wrap-item="false" align="center" inline size="small">

View File

@ -0,0 +1,72 @@
<script setup>
import { types } from '@/consts/value_view_type.js'
import { types as redisTypes } from '@/consts/support_redis_type.js'
import ContentValueString from '@/components/content_value/ContentValueString.vue'
import ContentValueHash from '@/components/content_value/ContentValueHash.vue'
import ContentValueList from '@/components/content_value/ContentValueList.vue'
import ContentValueSet from '@/components/content_value/ContentValueSet.vue'
import ContentValueZset from '@/components/content_value/ContentValueZSet.vue'
import ContentValueStream from '@/components/content_value/ContentValueStream.vue'
import { useThemeVars } from 'naive-ui'
const themeVars = useThemeVars()
const props = defineProps({
blank: Boolean,
type: String,
name: String,
db: Number,
keyPath: String,
keyCode: {
type: Array,
default: null,
},
ttl: {
type: Number,
default: -1,
},
value: [String, Object],
size: Number,
viewAs: {
type: String,
default: types.PLAIN_TEXT,
},
})
const emit = defineEmits(['reload'])
const valueComponents = {
[redisTypes.STRING]: ContentValueString,
[redisTypes.HASH]: ContentValueHash,
[redisTypes.LIST]: ContentValueList,
[redisTypes.SET]: ContentValueSet,
[redisTypes.ZSET]: ContentValueZset,
[redisTypes.STREAM]: ContentValueStream,
}
</script>
<template>
<n-empty v-if="props.blank" :description="$t('interface.nonexist_tab_content')" class="empty-content">
<template #extra>
<n-button :focusable="false" @click="emit('reload')">{{ $t('interface.reload') }}</n-button>
</template>
</n-empty>
<component
class="content-value-wrapper"
:is="valueComponents[props.type]"
v-else
:db="props.db"
:key-code="props.keyCode"
:key-path="props.keyPath"
:name="props.name"
:size="props.size"
:ttl="props.ttl"
:value="props.value"
:view-as="props.viewAs" />
</template>
<style scoped lang="scss">
.content-value-wrapper {
background-color: v-bind('themeVars.bodyColor');
}
</style>

View File

@ -0,0 +1,109 @@
<script setup>
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
inverse: {
type: Boolean,
default: false,
},
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg v-if="props.inverse" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="6"
y="6"
width="36"
height="36"
rx="3"
fill="currentColor"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<rect
x="13"
y="13"
width="8"
height="8"
fill="#FFF"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M27 13L35 13"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M27 20L35 20"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M13 28L35 28"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M13 35H35"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<svg v-else viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="6"
y="6"
width="36"
height="36"
rx="3"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<rect
x="13"
y="13"
width="8"
height="8"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M27 13L35 13"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M27 20L35 20"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M13 28L35 28"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M13 35H35"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,67 @@
<script setup>
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
inverse: {
type: Boolean,
default: false,
},
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg v-if="props.inverse" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="4"
y="8"
width="40"
height="32"
rx="2"
fill="currentColor"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M12 18L19 24L12 30"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M23 32H36"
stroke="#FFF"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<svg v-else viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="4"
y="8"
width="40"
height="32"
rx="2"
fill="none"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linejoin="round" />
<path
d="M12 18L19 24L12 30"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M23 32H36"
stroke="currentColor"
:stroke-width="props.strokeWidth"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,10 @@
/**
* all types of Browser sub tabs
* @enum {string}
*/
export const BrowserTabType = {
Status: 'status',
KeyDetail: 'key_detail',
Cli: 'cli',
SlowLog: 'slow_log',
}

View File

@ -87,13 +87,19 @@
"load_more": "Load More Keys", "load_more": "Load More Keys",
"load_all": "Load All Left Keys", "load_all": "Load All Left Keys",
"more_action": "More Action", "more_action": "More Action",
"nonexist_tab_content": "Selected key does not exist. Please retry", "nonexist_tab_content": "Selected key does not exist or no key is selected. Please retry",
"empty_server_content": "Select and open a connection from the left", "empty_server_content": "Select and open a connection from the left",
"empty_server_list": "No redis server", "empty_server_list": "No redis server",
"action": "Action", "action": "Action",
"type": "Type", "type": "Type",
"score": "Score", "score": "Score",
"total": "Length: {size}" "total": "Length: {size}",
"sub_tab": {
"status": "Status",
"key_detail": "Key Detail",
"cli": "Command Line",
"slow_log": "Slow Log"
}
}, },
"ribbon": { "ribbon": {
"server": "Server", "server": "Server",

View File

@ -87,13 +87,19 @@
"load_more": "加载更多键", "load_more": "加载更多键",
"load_all": "加载剩余所有键", "load_all": "加载剩余所有键",
"more_action": "更多操作", "more_action": "更多操作",
"nonexist_tab_content": "所选键不存在,请尝试刷新重试", "nonexist_tab_content": "所选键不存在或未选中任何键,请尝试刷新重试",
"empty_server_content": "可以从左边选择并打开连接", "empty_server_content": "可以从左边选择并打开连接",
"empty_server_list": "还没添加Redis服务器", "empty_server_list": "还没添加Redis服务器",
"action": "操作", "action": "操作",
"type": "类型", "type": "类型",
"score": "分值", "score": "分值",
"total": "总数:{size}" "total": "总数:{size}",
"sub_tab": {
"status": "状态",
"key_detail": "键详情",
"cli": "命令行",
"slow_log": "慢日志"
}
}, },
"ribbon": { "ribbon": {
"server": "服务器", "server": "服务器",

View File

@ -52,7 +52,7 @@ import useTabStore from './tab.js'
import { types } from '@/consts/support_redis_type.js' import { types } from '@/consts/support_redis_type.js'
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js' import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
import { KeyViewType } from '@/consts/key_view_type.js' import { KeyViewType } from '@/consts/key_view_type.js'
import { nextTick } from 'vue' import { BrowserTabType } from '@/consts/browser_tab_type.js'
const useConnectionStore = defineStore('connections', { const useConnectionStore = defineStore('connections', {
/** /**
@ -668,6 +668,7 @@ const useConnectionStore = defineStore('connections', {
const k = decodeRedisKey(key) const k = decodeRedisKey(key)
const binaryKey = k !== key const binaryKey = k !== key
tab.upsertTab({ tab.upsertTab({
subTab: BrowserTabType.KeyDetail,
server, server,
db, db,
type, type,
@ -690,6 +691,7 @@ const useConnectionStore = defineStore('connections', {
} }
tab.upsertTab({ tab.upsertTab({
subTab: BrowserTabType.Status,
server, server,
db, db,
type: 'none', type: 'none',

View File

@ -1,4 +1,4 @@
import { find, findIndex, get, size } from 'lodash' import { find, findIndex, get, isEmpty, set, size } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
const useTabStore = defineStore('tab', { const useTabStore = defineStore('tab', {
@ -6,6 +6,7 @@ const useTabStore = defineStore('tab', {
* @typedef {Object} TabItem * @typedef {Object} TabItem
* @property {string} name connection name * @property {string} name connection name
* @property {boolean} blank is blank tab * @property {boolean} blank is blank tab
* @property {string} subTab secondary tab value
* @property {string} [title] tab title * @property {string} [title] tab title
* @property {string} [icon] tab icon * @property {string} [icon] tab icon
* @property {string[]} selectedKeys * @property {string[]} selectedKeys
@ -64,12 +65,16 @@ const useTabStore = defineStore('tab', {
* *
* @param idx * @param idx
* @param {boolean} [switchNav] * @param {boolean} [switchNav]
* @param {string} [subTab]
* @private * @private
*/ */
_setActivatedIndex(idx, switchNav) { _setActivatedIndex(idx, switchNav, subTab) {
this.activatedIndex = idx this.activatedIndex = idx
if (switchNav === true) { if (switchNav === true) {
this.nav = idx >= 0 ? 'browser' : 'server' this.nav = idx >= 0 ? 'browser' : 'server'
if (!isEmpty(subTab)) {
set(this.tabList, [idx, 'subTab'], subTab)
}
} else { } else {
if (idx < 0) { if (idx < 0) {
this.nav = 'server' this.nav = 'server'
@ -79,6 +84,7 @@ const useTabStore = defineStore('tab', {
/** /**
* update or insert a new tab if not exists with the same name * update or insert a new tab if not exists with the same name
* @param {string} subTab
* @param {string} server * @param {string} server
* @param {number} [db] * @param {number} [db]
* @param {number} [type] * @param {number} [type]
@ -89,11 +95,13 @@ const useTabStore = defineStore('tab', {
* @param {*} [value] * @param {*} [value]
* @param {string} [viewAs] * @param {string} [viewAs]
*/ */
upsertTab({ server, db, type, ttl, key, keyCode, size, value, viewAs }) { upsertTab({ subTab, server, db, type, ttl, key, keyCode, size, value, viewAs }) {
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({
name: server, name: server,
title: server,
subTab,
server, server,
db, db,
type, type,
@ -105,21 +113,23 @@ const useTabStore = defineStore('tab', {
viewAs, viewAs,
}) })
tabIndex = this.tabList.length - 1 tabIndex = this.tabList.length - 1
} else {
const tab = this.tabList[tabIndex]
tab.blank = false
tab.subTab = subTab
// tab.title = db !== undefined ? `${server}/db${db}` : `${server}`
tab.title = server
tab.server = server
tab.db = db
tab.type = type
tab.ttl = ttl
tab.key = key
tab.keyCode = keyCode
tab.size = size
tab.value = value
tab.viewAs = viewAs
} }
const tab = this.tabList[tabIndex] this._setActivatedIndex(tabIndex, true, subTab)
tab.blank = false
// tab.title = db !== undefined ? `${server}/db${db}` : `${server}`
tab.title = server
tab.server = server
tab.db = db
tab.type = type
tab.ttl = ttl
tab.key = key
tab.keyCode = keyCode
tab.size = size
tab.value = value
tab.viewAs = viewAs
this._setActivatedIndex(tabIndex, true)
// this.activatedTab = tab.name // this.activatedTab = tab.name
}, },
@ -162,6 +172,14 @@ const useTabStore = defineStore('tab', {
// this.activatedIndex = tabIndex // this.activatedIndex = tabIndex
}, },
switchSubTab(name) {
const tab = this.currentTab
if (tab == null) {
return
}
tab.subTab = name
},
/** /**
* *
* @param {number} tabIndex * @param {number} tabIndex

View File

@ -1,8 +1,6 @@
.content-container { .content-container {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
padding-top: 2px;
padding-bottom: 5px;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -75,7 +75,7 @@ body {
} }
.content-wrapper { .content-wrapper {
//height: 100%; height: 100%;
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
gap: 5px; gap: 5px;
@ -95,6 +95,7 @@ body {
.value-wrapper { .value-wrapper {
//border-top: v-bind('themeVars.borderColor') 1px solid; //border-top: v-bind('themeVars.borderColor') 1px solid;
user-select: text; user-select: text;
height: 100%;
} }
} }