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>
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 useTabStore from 'stores/tab.js'
import useConnectionStore from 'stores/connections.js'
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
@ -112,15 +115,6 @@ onUnmounted(() => {
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 tabStore = useTabStore()
const tab = computed(() =>
@ -162,6 +156,7 @@ const tabContent = computed(() => {
}
return {
name: tab.name,
subTab: tab.subTab,
type: toUpper(tab.type),
db: tab.db,
keyPath: tab.key,
@ -177,7 +172,7 @@ const showServerStatus = computed(() => {
return tabContent.value == null || isEmpty(tabContent.value.keyPath)
})
const showNonexists = computed(() => {
const isBlankValue = computed(() => {
return tabContent.value.value == null
})
@ -192,38 +187,104 @@ const onReloadKey = async () => {
}
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>
<template>
<div class="content-container flex-box-v">
<div v-if="showServerStatus" class="content-container flex-item-expand flex-box-v">
<!-- select nothing or select server node, display server status -->
<content-server-status
v-model:auto-refresh="currentServer.autoRefresh"
:auto-loading="currentServer.autoLoading"
:info="currentServer.info"
:loading="currentServer.loading"
:server="currentServer.name"
@refresh="refreshInfo(currentServer.name, true)" />
</div>
<div v-else-if="showNonexists" class="content-container flex-item-expand flex-box-v">
<n-empty :description="$t('interface.nonexist_tab_content')" class="empty-content">
<template #extra>
<n-button :focusable="false" @click="onReloadKey">{{ $t('interface.reload') }}</n-button>
<n-tabs
:tabs-padding="5"
:theme-overrides="{
tabGapSmallLine: '10px',
tabGapMediumLine: '10px',
tabGapLargeLine: '10px',
}"
:value="selectedSubTab"
class="content-sub-tab"
default-value="status"
pane-class="content-sub-tab-pane"
placement="top"
tab-style="padding-left: 10px; padding-right: 10px;"
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>
</n-empty>
</div>
<component
:is="valueComponents[tabContent.type]"
v-else
: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" />
<content-server-status
v-model:auto-refresh="currentServer.autoRefresh"
:auto-loading="currentServer.autoLoading"
:info="currentServer.info"
:loading="currentServer.loading"
:server="currentServer.name"
@refresh="refreshInfo(currentServer.name, true)" />
</n-tab-pane>
<!-- key detail pane -->
<n-tab-pane :name="BrowserTabType.KeyDetail.toString()">
<template #tab>
<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>
</template>
@ -231,27 +292,24 @@ const onReloadKey = async () => {
@import '@/styles/content';
.content-container {
padding: 5px;
//padding: 5px 5px 0;
//padding-top: 0;
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 {
// gap: 5px;
// padding: 0 5px 0 10px;
// align-items: center;
// max-width: 150px;
//
// 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);
// }
// }
//}
.content-sub-tab-pane {
padding: 0 !important;
height: 100%;
background-color: v-bind('themeVars.tabColor');
overflow: hidden;
}
</style>

View File

@ -83,7 +83,7 @@ const tab = computed(() =>
@dblclick.stop="() => {}">
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="18">
<server stroke-width="4" :inverse="tabStore.activatedIndex === i" />
<server stroke-width="4" />
</n-icon>
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
</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>
<n-scrollbar ref="scrollRef">
<n-back-top :listen-to="scrollRef" />
<n-space vertical>
<n-space vertical :wrap-item="false" :size="5" style="padding: 5px">
<n-card>
<template #header>
<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_all": "Load All Left Keys",
"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_list": "No redis server",
"action": "Action",
"type": "Type",
"score": "Score",
"total": "Length: {size}"
"total": "Length: {size}",
"sub_tab": {
"status": "Status",
"key_detail": "Key Detail",
"cli": "Command Line",
"slow_log": "Slow Log"
}
},
"ribbon": {
"server": "Server",

View File

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

View File

@ -52,7 +52,7 @@ import useTabStore from './tab.js'
import { types } from '@/consts/support_redis_type.js'
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.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', {
/**
@ -668,6 +668,7 @@ const useConnectionStore = defineStore('connections', {
const k = decodeRedisKey(key)
const binaryKey = k !== key
tab.upsertTab({
subTab: BrowserTabType.KeyDetail,
server,
db,
type,
@ -690,6 +691,7 @@ const useConnectionStore = defineStore('connections', {
}
tab.upsertTab({
subTab: BrowserTabType.Status,
server,
db,
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'
const useTabStore = defineStore('tab', {
@ -6,6 +6,7 @@ const useTabStore = defineStore('tab', {
* @typedef {Object} TabItem
* @property {string} name connection name
* @property {boolean} blank is blank tab
* @property {string} subTab secondary tab value
* @property {string} [title] tab title
* @property {string} [icon] tab icon
* @property {string[]} selectedKeys
@ -64,12 +65,16 @@ const useTabStore = defineStore('tab', {
*
* @param idx
* @param {boolean} [switchNav]
* @param {string} [subTab]
* @private
*/
_setActivatedIndex(idx, switchNav) {
_setActivatedIndex(idx, switchNav, subTab) {
this.activatedIndex = idx
if (switchNav === true) {
this.nav = idx >= 0 ? 'browser' : 'server'
if (!isEmpty(subTab)) {
set(this.tabList, [idx, 'subTab'], subTab)
}
} else {
if (idx < 0) {
this.nav = 'server'
@ -79,6 +84,7 @@ const useTabStore = defineStore('tab', {
/**
* update or insert a new tab if not exists with the same name
* @param {string} subTab
* @param {string} server
* @param {number} [db]
* @param {number} [type]
@ -89,11 +95,13 @@ const useTabStore = defineStore('tab', {
* @param {*} [value]
* @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 })
if (tabIndex === -1) {
this.tabList.push({
name: server,
title: server,
subTab,
server,
db,
type,
@ -105,21 +113,23 @@ const useTabStore = defineStore('tab', {
viewAs,
})
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]
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._setActivatedIndex(tabIndex, true, subTab)
// this.activatedTab = tab.name
},
@ -162,6 +172,14 @@ const useTabStore = defineStore('tab', {
// this.activatedIndex = tabIndex
},
switchSubTab(name) {
const tab = this.currentTab
if (tab == null) {
return
}
tab.subTab = name
},
/**
*
* @param {number} tabIndex

View File

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

View File

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