Refactor server and database save structure, support multiple server detail tab

This commit is contained in:
tiny-craft 2023-07-01 02:05:30 +08:00
parent 79784fd109
commit dadde8d090
40 changed files with 1634 additions and 306 deletions

View File

@ -1,21 +1,25 @@
<script setup>
import ContentPane from './components/ContentPane.vue'
import NavigationPane from './components/NavigationPane.vue'
import ContentPane from './components/content/ContentPane.vue'
import DatabasePane from './components/sidebar/DatabasePane.vue'
import { computed, nextTick, onMounted, provide, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { GetPreferences } from '../wailsjs/go/storage/PreferencesStorage.js'
import { get } from 'lodash'
import { useThemeVars } from 'naive-ui'
import NavMenu from './components/NavMenu.vue'
import ConnectionPane from './components/sidebar/ConnectionPane.vue'
import ContentServerPane from './components/content/ContentServerPane.vue'
import useTabStore from './stores/tab.js'
const themeVars = useThemeVars()
const data = reactive({
asideWith: 300,
navMenuWidth: 60,
hoverResize: false,
resizing: false,
})
const tabStore = useTabStore()
const preferences = ref({})
provide('preferences', preferences)
const i18n = useI18n()
@ -34,7 +38,7 @@ const getFontSize = computed(() => {
const handleResize = (evt) => {
if (data.resizing) {
data.asideWith = Math.max(evt.clientX, 300)
tabStore.asideWidth = Math.max(evt.clientX - data.navMenuWidth, 300)
}
}
@ -52,7 +56,7 @@ const startResize = () => {
}
const asideWidthVal = computed(() => {
return data.asideWith + 'px'
return tabStore.asideWidth + 'px'
})
const dragging = computed(() => {
@ -62,22 +66,51 @@ const dragging = computed(() => {
<template>
<!-- app content-->
<div id="app-container" :class="{ dragging: dragging }" class="flex-box-h">
<nav-menu />
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
<navigation-pane class="flex-item-expand"></navigation-pane>
<div
:class="{
'resize-divider-hover': data.hoverResize,
'resize-divider-drag': data.resizing,
}"
class="resize-divider"
@mousedown="startResize"
@mouseout="data.hoverResize = false"
@mouseover="data.hoverResize = true"
></div>
<div id="app-container" :class="{ dragging }" class="flex-box-h">
<nav-menu v-model:value="tabStore.nav" :width="data.navMenuWidth" />
<!-- structure page-->
<div v-show="tabStore.nav === 'structure'" class="flex-box-h flex-item-expand">
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
<database-pane
v-for="t in tabStore.tabs"
v-show="get(tabStore.currentTab, 'name') === t.name"
:key="t.name"
class="flex-item-expand"
/>
<div
:class="{
'resize-divider-hover': data.hoverResize,
'resize-divider-drag': data.resizing,
}"
class="resize-divider"
@mousedown="startResize"
@mouseout="data.hoverResize = false"
@mouseover="data.hoverResize = true"
/>
</div>
<content-pane class="flex-item-expand" />
</div>
<content-pane class="flex-item-expand" />
<!-- server list page -->
<div v-show="tabStore.nav === 'server'" class="flex-box-h flex-item-expand">
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
<connection-pane class="flex-item-expand" />
<div
:class="{
'resize-divider-hover': data.hoverResize,
'resize-divider-drag': data.resizing,
}"
class="resize-divider"
@mousedown="startResize"
@mouseout="data.hoverResize = false"
@mouseover="data.hoverResize = true"
/>
</div>
<content-server-pane class="flex-item-expand" />
</div>
<!-- log page -->
<div v-show="tabStore.nav === 'log'">display log</div>
</div>
</template>

View File

@ -5,11 +5,11 @@ import Delete from './icons/Delete.vue'
import Edit from './icons/Edit.vue'
import Refresh from './icons/Refresh.vue'
import Timer from './icons/Timer.vue'
import RedisTypeTag from './RedisTypeTag.vue'
import useConnectionStore from '../stores/connection.js'
import RedisTypeTag from './common/RedisTypeTag.vue'
import { useI18n } from 'vue-i18n'
import { useMessage } from 'naive-ui'
import IconButton from './IconButton.vue'
import IconButton from './common/IconButton.vue'
import useConnectionStore from '../stores/connections.js'
const props = defineProps({
server: String,

View File

@ -1,23 +1,38 @@
<script setup>
import { computed, h, ref } from 'vue'
import { computed, h } from 'vue'
import { NIcon, useThemeVars } from 'naive-ui'
import ToggleDb from './icons/ToggleDb.vue'
import { useI18n } from 'vue-i18n'
import ToggleServer from './icons/ToggleServer.vue'
import IconButton from './IconButton.vue'
import IconButton from './common/IconButton.vue'
import Config from './icons/Config.vue'
import useDialogStore from '../stores/dialog.js'
import Github from './icons/Github.vue'
import { BrowserOpenURL } from '../../wailsjs/runtime/runtime.js'
import Log from './icons/Log.vue'
import useConnectionStore from '../stores/connections.js'
const themeVars = useThemeVars()
const iconSize = 26
const selectedMenu = ref('server')
const props = defineProps({
value: {
type: String,
default: 'server',
},
width: {
type: Number,
default: 60,
},
})
const emit = defineEmits(['update:value'])
const iconSize = computed(() => Math.floor(props.width * 0.4))
const renderIcon = (icon) => {
return () => h(NIcon, null, { default: () => h(icon) })
}
const connectionStore = useConnectionStore()
const i18n = useI18n()
const menuOptions = computed(() => {
return [
@ -25,13 +40,18 @@ const menuOptions = computed(() => {
label: i18n.t('structure'),
key: 'structure',
icon: renderIcon(ToggleDb),
show: true,
show: connectionStore.anyConnectionOpened,
},
{
label: i18n.t('server'),
key: 'server',
icon: renderIcon(ToggleServer),
},
{
label: i18n.t('log'),
key: 'log',
icon: renderIcon(Log),
},
]
})
@ -74,12 +94,19 @@ const openGithub = () => {
</script>
<template>
<div id="app-nav-menu" class="flex-box-v">
<div
id="app-nav-menu"
:style="{
width: props.width + 'px',
}"
class="flex-box-v"
>
<n-menu
v-model:value="selectedMenu"
:collapsed-width="props.width"
:value="props.value"
:collapsed="true"
:collapsed-icon-size="iconSize"
:collapsed-width="60"
@update:value="(val) => emit('update:value', val)"
:options="menuOptions"
></n-menu>
<div class="flex-item-expand"></div>
@ -101,7 +128,7 @@ const openGithub = () => {
<style lang="scss">
#app-nav-menu {
width: 60px;
//width: 60px;
height: 100vh;
border-right: var(--border-color) solid 1px;

View File

@ -1,9 +1,9 @@
<script setup>
import IconButton from './IconButton.vue'
import Delete from './icons/Delete.vue'
import Edit from './icons/Edit.vue'
import Close from './icons/Close.vue'
import Save from './icons/Save.vue'
import Delete from '../icons/Delete.vue'
import Edit from '../icons/Edit.vue'
import Close from '../icons/Close.vue'
import Save from '../icons/Save.vue'
const props = defineProps({
bindKey: String,

View File

@ -1,6 +1,6 @@
<script setup>
import { computed } from 'vue'
import { types, validType } from '../consts/support_redis_type.js'
import { types, validType } from '../../consts/support_redis_type.js'
const props = defineProps({
type: {

View File

@ -1,13 +1,16 @@
<script setup>
import { computed } from 'vue'
import { types } from '../consts/support_redis_type.js'
import ContentValueHash from './content_value/ContentValueHash.vue'
import ContentValueList from './content_value/ContentValueList.vue'
import ContentValueString from './content_value/ContentValueString.vue'
import ContentValueSet from './content_value/ContentValueSet.vue'
import ContentValueZset from './content_value/ContentValueZset.vue'
import { types } from '../../consts/support_redis_type.js'
import ContentValueHash from '../content_value/ContentValueHash.vue'
import ContentValueList from '../content_value/ContentValueList.vue'
import ContentValueString from '../content_value/ContentValueString.vue'
import ContentValueSet from '../content_value/ContentValueSet.vue'
import ContentValueZset from '../content_value/ContentValueZSet.vue'
import { isEmpty, map, toUpper } from 'lodash'
import useTabStore from '../stores/tab.js'
import useTabStore from '../../stores/tab.js'
import { useDialog } from 'naive-ui'
import useConnectionStore from '../../stores/connections.js'
import { useI18n } from 'vue-i18n'
const valueComponents = {
[types.STRING]: ContentValueString,
@ -17,6 +20,8 @@ const valueComponents = {
[types.ZSET]: ContentValueZset,
}
const dialog = useDialog()
const connectionStore = useConnectionStore()
const tabStore = useTabStore()
const tab = computed(() =>
map(tabStore.tabs, (item) => ({
@ -52,9 +57,24 @@ const onAddTab = () => {
tabStore.newBlankTab()
}
const i18n = useI18n()
const onCloseTab = (tabIndex) => {
tabStore.removeTab(tabIndex)
console.log('TODO: close connection also')
dialog.warning({
title: i18n.t('close_confirm_title'),
content: i18n.t('close_confirm'),
positiveText: i18n.t('confirm'),
negativeText: i18n.t('cancel'),
closable: false,
closeOnEsc: false,
maskClosable: false,
transformOrigin: 'center',
onPositiveClick: () => {
const removedTab = tabStore.removeTab(tabIndex)
if (removedTab != null) {
connectionStore.closeConnection(removedTab.name)
}
},
})
}
</script>
@ -63,8 +83,7 @@ const onCloseTab = (tabIndex) => {
<!-- <content-tab :model-value="tab"></content-tab>-->
<n-tabs
v-model:value="tabStore.activatedIndex"
:closable="tab.length > 1"
addable
:closable="true"
size="small"
type="card"
@add="onAddTab"
@ -75,10 +94,10 @@ const onCloseTab = (tabIndex) => {
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
</n-tab>
</n-tabs>
<!-- add loading status -->
<!-- TODO: add loading status -->
<component
:is="valueComponents[tabContent.type]"
v-if="tabContent != null && !isEmpty(tabContent.keyPath)"
:is="valueComponents[tabContent.type]"
:db="tabContent.db"
:key-path="tabContent.keyPath"
:name="tabContent.name"
@ -92,20 +111,5 @@ const onCloseTab = (tabIndex) => {
</template>
<style lang="scss" scoped>
.content-container {
height: 100%;
overflow: hidden;
background-color: var(--bg-color);
padding-top: 2px;
padding-bottom: 5px;
box-sizing: border-box;
}
.empty-content {
height: 100%;
justify-content: center;
}
.tab-content {
}
@import 'content';
</style>

View File

@ -0,0 +1,43 @@
<script setup>
import useDialog from '../../stores/dialog.js'
import AddLink from '../icons/AddLink.vue'
const dialogStore = useDialog()
</script>
<template>
<div class="content-container flex-box-v">
<!-- TODO: replace icon to app icon -->
<n-empty :description="$t('empty_server_content')">
<template #extra>
<n-button @click="dialogStore.openNewDialog()">
<template #icon>
<n-icon :component="AddLink" size="18" />
</template>
{{ $t('new_conn') }}
</n-button>
</template>
</n-empty>
</div>
</template>
<style lang="scss" scoped>
@import 'content';
.content-container {
justify-content: center;
}
.color-preset-item {
width: 24px;
height: 24px;
margin-right: 2px;
border: white 3px solid;
cursor: pointer;
&_selected,
&:hover {
border-color: #cdd0d6;
}
}
</style>

View File

@ -1,9 +1,8 @@
<script setup>
import { ref, watch } from 'vue'
import useConnectionStore from '../stores/connection.js'
import { throttle } from 'lodash'
import { ConnectionType } from '../consts/connection_type.js'
import Close from './icons/Close.vue'
import { ref } from 'vue'
import { ConnectionType } from '../../consts/connection_type.js'
import Close from '../icons/Close.vue'
import useConnectionStore from '../../stores/connections.js'
const emit = defineEmits(['switchTab', 'closeTab', 'update:modelValue'])
@ -31,7 +30,7 @@ const onCurrentSelectChange = ({ type, group = '', server = '', db = 0, key = ''
// load and update content value
}
}
watch(() => connectionStore.currentSelect, throttle(onCurrentSelectChange, 1000))
// watch(() => databaseStore.currentSelect, throttle(onCurrentSelectChange, 1000))
const items = ref(props.modelValue)
const selIndex = ref(props.selectedIndex)

View File

@ -0,0 +1,16 @@
.content-container {
height: 100%;
overflow: hidden;
background-color: var(--bg-color);
padding-top: 2px;
padding-bottom: 5px;
box-sizing: border-box;
}
.empty-content {
height: 100%;
justify-content: center;
}
.tab-content {
}

View File

@ -5,9 +5,9 @@ import ContentToolbar from '../ContentToolbar.vue'
import AddLink from '../icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, useMessage } from 'naive-ui'
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
import EditableTableColumn from '../EditableTableColumn.vue'
import useConnectionStore from '../../stores/connection.js'
import EditableTableColumn from '../common/EditableTableColumn.vue'
import useDialogStore from '../../stores/dialog.js'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()

View File

@ -6,9 +6,9 @@ import AddLink from '../icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, useMessage } from 'naive-ui'
import { size } from 'lodash'
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
import EditableTableColumn from '../EditableTableColumn.vue'
import useConnectionStore from '../../stores/connection.js'
import EditableTableColumn from '../common/EditableTableColumn.vue'
import useDialogStore from '../../stores/dialog.js'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()

View File

@ -5,10 +5,10 @@ import ContentToolbar from '../ContentToolbar.vue'
import AddLink from '../icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, useMessage } from 'naive-ui'
import { size } from 'lodash'
import useConnectionStore from '../../stores/connection.js'
import useDialogStore from '../../stores/dialog.js'
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
import EditableTableColumn from '../EditableTableColumn.vue'
import EditableTableColumn from '../common/EditableTableColumn.vue'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()
const props = defineProps({

View File

@ -9,10 +9,10 @@ import { types } from '../../consts/value_view_type.js'
import Close from '../icons/Close.vue'
import Edit from '../icons/Edit.vue'
import { IsJson } from '../../utils/check_string_format.js'
import useConnectionStore from '../../stores/connection.js'
import { types as redisTypes } from '../../consts/support_redis_type.js'
import { ClipboardSetText } from '../../../wailsjs/runtime/runtime.js'
import { toLower } from 'lodash'
import useConnectionStore from '../../stores/connections.js'
const props = defineProps({
name: String,

View File

@ -5,10 +5,10 @@ import ContentToolbar from '../ContentToolbar.vue'
import AddLink from '../icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, NInputNumber, useMessage } from 'naive-ui'
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
import EditableTableColumn from '../EditableTableColumn.vue'
import useConnectionStore from '../../stores/connection.js'
import EditableTableColumn from '../common/EditableTableColumn.vue'
import { isEmpty } from 'lodash'
import useDialogStore from '../../stores/dialog.js'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()
const props = defineProps({

View File

@ -4,12 +4,12 @@ import { types } from '../../consts/support_redis_type'
import useDialog from '../../stores/dialog'
import NewStringValue from '../new_value/NewStringValue.vue'
import NewSetValue from '../new_value/NewSetValue.vue'
import useConnectionStore from '../../stores/connection.js'
import { useI18n } from 'vue-i18n'
import { useMessage } from 'naive-ui'
import AddListValue from '../new_value/AddListValue.vue'
import AddHashValue from '../new_value/AddHashValue.vue'
import AddZSetValue from '../new_value/AddZSetValue.vue'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()
const newForm = reactive({

View File

@ -8,8 +8,8 @@ import NewHashValue from '../new_value/NewHashValue.vue'
import NewListValue from '../new_value/NewListValue.vue'
import NewZSetValue from '../new_value/NewZSetValue.vue'
import NewSetValue from '../new_value/NewSetValue.vue'
import useConnectionStore from '../../stores/connection.js'
import { useI18n } from 'vue-i18n'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()
const newForm = reactive({

View File

@ -1,9 +1,9 @@
<script setup>
import { reactive, watch } from 'vue'
import useDialog from '../../stores/dialog'
import useConnectionStore from '../../stores/connection.js'
import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import useConnectionStore from '../../stores/connections.js'
const renameForm = reactive({
server: '',

View File

@ -2,8 +2,8 @@
import { reactive, ref, watch } from 'vue'
import useDialog from '../../stores/dialog'
import useTabStore from '../../stores/tab.js'
import useConnectionStore from '../../stores/connection.js'
import { useMessage } from 'naive-ui'
import useConnectionStore from '../../stores/connections.js'
const ttlForm = reactive({
ttl: -1,

View File

@ -0,0 +1,46 @@
<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">
<rect
:stroke-width="strokeWidth"
fill="none"
height="34"
stroke="currentColor"
stroke-linejoin="round"
width="28"
x="13"
y="10"
/>
<path
:stroke-width="strokeWidth"
d="M35 10V4H8C7.44772 4 7 4.44772 7 5V38H13"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
:stroke-width="strokeWidth"
d="M21 22H33"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
:stroke-width="strokeWidth"
d="M21 30H33"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { flatMap, reject } from 'lodash'
import Add from '../icons/Add.vue'
import Delete from '../icons/Delete.vue'
import IconButton from '../IconButton.vue'
import IconButton from '../common/IconButton.vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { compact } from 'lodash'
import Add from '../icons/Add.vue'
import Delete from '../icons/Delete.vue'
import IconButton from '../IconButton.vue'
import IconButton from '../common/IconButton.vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { isEmpty, reject } from 'lodash'
import Add from '../icons/Add.vue'
import Delete from '../icons/Delete.vue'
import IconButton from '../IconButton.vue'
import IconButton from '../common/IconButton.vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { flatMap, reject } from 'lodash'
import Add from '../icons/Add.vue'
import Delete from '../icons/Delete.vue'
import IconButton from '../IconButton.vue'
import IconButton from '../common/IconButton.vue'
const props = defineProps({
value: Array,

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { compact } from 'lodash'
import Add from '../icons/Add.vue'
import Delete from '../icons/Delete.vue'
import IconButton from '../IconButton.vue'
import IconButton from '../common/IconButton.vue'
const props = defineProps({
value: Array,

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { compact, uniq } from 'lodash'
import Add from '../icons/Add.vue'
import Delete from '../icons/Delete.vue'
import IconButton from '../IconButton.vue'
import IconButton from '../common/IconButton.vue'
const props = defineProps({
value: Array,

View File

@ -3,7 +3,7 @@ import { ref } from 'vue'
import { flatMap, isEmpty, reject } from 'lodash'
import Add from '../icons/Add.vue'
import Delete from '../icons/Delete.vue'
import IconButton from '../IconButton.vue'
import IconButton from '../common/IconButton.vue'
const props = defineProps({
value: Array,

View File

@ -1,12 +1,12 @@
<script setup>
import useDialogStore from '../stores/dialog.js'
import useDialogStore from '../../stores/dialog.js'
import { NIcon } from 'naive-ui'
import AddGroup from './icons/AddGroup.vue'
import AddLink from './icons/AddLink.vue'
import Sort from './icons/Sort.vue'
import ConnectionsTree from './ConnectionsTree.vue'
import IconButton from './IconButton.vue'
import Filter from './icons/Filter.vue'
import AddGroup from '../icons/AddGroup.vue'
import AddLink from '../icons/AddLink.vue'
import Sort from '../icons/Sort.vue'
import IconButton from '../common/IconButton.vue'
import Filter from '../icons/Filter.vue'
import ConnectionTree from './ConnectionTree.vue'
const dialogStore = useDialogStore()
@ -16,8 +16,8 @@ const onSort = () => {
</script>
<template>
<div v-if="true" class="nav-pane-container flex-box-v">
<ConnectionsTree />
<div class="nav-pane-container flex-box-v">
<connection-tree />
<!-- bottom function bar -->
<div class="nav-pane-bottom flex-box-h">

View File

@ -0,0 +1,271 @@
<script setup>
import useDialogStore from '../../stores/dialog.js'
import { h, nextTick, onMounted, reactive, ref } from 'vue'
import useConnectionStore from '../../stores/connections.js'
import { NIcon, useMessage } from 'naive-ui'
import { ConnectionType } from '../../consts/connection_type.js'
import ToggleFolder from '../icons/ToggleFolder.vue'
import ToggleServer from '../icons/ToggleServer.vue'
import { indexOf } from 'lodash'
import Config from '../icons/Config.vue'
import Delete from '../icons/Delete.vue'
import Refresh from '../icons/Refresh.vue'
import Unlink from '../icons/Unlink.vue'
import CopyLink from '../icons/CopyLink.vue'
import Connect from '../icons/Connect.vue'
import { useI18n } from 'vue-i18n'
import useTabStore from '../../stores/tab.js'
import Edit from '../icons/Edit.vue'
const i18n = useI18n()
const loadingConnection = ref(false)
const openingConnection = ref(false)
const connectionStore = useConnectionStore()
const tabStore = useTabStore()
const dialogStore = useDialogStore()
const message = useMessage()
const expandedKeys = ref([])
onMounted(async () => {
try {
loadingConnection.value = true
await nextTick()
await connectionStore.initConnections()
} finally {
loadingConnection.value = false
}
})
const contextMenuParam = reactive({
show: false,
x: 0,
y: 0,
options: null,
currentNode: null,
})
const renderIcon = (icon) => {
return () => {
return h(NIcon, null, {
default: () => h(icon),
})
}
}
const menuOptions = {
[ConnectionType.Group]: ({ opened }) => [
{
key: 'group_reload',
label: i18n.t('edit_conn_group'),
icon: renderIcon(Config),
},
{
key: 'group_delete',
label: i18n.t('remove_conn_group'),
icon: renderIcon(Delete),
},
],
[ConnectionType.Server]: ({ connected }) => {
if (connected) {
return [
{
key: 'server_reload',
label: i18n.t('reload'),
icon: renderIcon(Refresh),
},
{
key: 'server_close',
label: i18n.t('disconnect'),
icon: renderIcon(Unlink),
},
{
key: 'server_dup',
label: i18n.t('dup_conn'),
icon: renderIcon(CopyLink),
},
{
key: 'server_config',
label: i18n.t('edit_conn'),
icon: renderIcon(Config),
},
{
type: 'divider',
key: 'd1',
},
{
key: 'server_remove',
label: i18n.t('remove_conn'),
icon: renderIcon(Delete),
},
]
} else {
return [
{
key: 'server_open',
label: i18n.t('open_connection'),
icon: renderIcon(Connect),
},
{
key: 'server_edit',
label: i18n.t('edit_conn'),
icon: renderIcon(Edit),
},
{
type: 'divider',
key: 'd1',
},
{
key: 'server_delete',
label: i18n.t('remove_conn'),
icon: renderIcon(Delete),
},
]
}
},
}
const renderLabel = ({ option }) => {
// switch (option.type) {
// case ConnectionType.Server:
// return h(ConnectionTreeItem, { title: option.label })
// }
return option.label
}
const renderPrefix = ({ option }) => {
switch (option.type) {
case ConnectionType.Group:
const opened = indexOf(expandedKeys.value, option.key) !== -1
return h(
NIcon,
{ size: 20 },
{
default: () => h(ToggleFolder, { modelValue: opened }),
}
)
case ConnectionType.Server:
const connected = connectionStore.isConnected(option.name)
return h(
NIcon,
{ size: 20 },
{
default: () => h(ToggleServer, { modelValue: !!connected }),
}
)
}
}
const onUpdateExpandedKeys = (value, option, meta) => {
expandedKeys.value = value
}
/**
* Open connection
* @param name
* @returns {Promise<void>}
*/
const openConnection = async (name) => {
try {
openingConnection.value = true
await connectionStore.openConnection(name)
tabStore.upsertTab({
server: name,
})
} catch (e) {
message.error(e.message)
// node.isLeaf = undefined
} finally {
openingConnection.value = false
}
}
const nodeProps = ({ option }) => {
return {
onDblclick: async () => {
if (option.type === ConnectionType.Server) {
openConnection(option.name).then(() => {})
}
},
onContextmenu(e) {
e.preventDefault()
const mop = menuOptions[option.type]
if (mop == null) {
return
}
contextMenuParam.show = false
nextTick().then(() => {
contextMenuParam.options = mop(option)
contextMenuParam.currentNode = option
contextMenuParam.x = e.clientX
contextMenuParam.y = e.clientY
contextMenuParam.show = true
})
},
}
}
const renderContextLabel = (option) => {
return h('div', { class: 'context-menu-item' }, option.label)
}
const handleSelectContextMenu = (key) => {
contextMenuParam.show = false
const { name, db, key: nodeKey, redisKey } = contextMenuParam.currentNode
switch (key) {
case 'server_open':
openConnection(name).then(() => {})
}
console.warn('TODO: handle context menu:' + key)
}
</script>
<template>
<n-tree
:animated="false"
:block-line="true"
:block-node="true"
:cancelable="false"
:data="connectionStore.connections"
:expand-on-click="true"
:expanded-keys="expandedKeys"
:node-props="nodeProps"
:on-update:expanded-keys="onUpdateExpandedKeys"
:render-label="renderLabel"
:render-prefix="renderPrefix"
class="fill-height"
virtual-scroll
/>
<!-- status display modal -->
<n-modal :show="loadingConnection || openingConnection" transform-origin="center">
<n-card
:bordered="false"
:content-style="{ textAlign: 'center' }"
aria-model="true"
role="dialog"
style="width: 400px"
>
<n-spin>
<template #description>
{{ openingConnection ? $t('opening_connection') : '' }}
</template>
</n-spin>
</n-card>
</n-modal>
<!-- context menu -->
<n-dropdown
:animated="false"
:options="contextMenuParam.options"
:render-label="renderContextLabel"
:show="contextMenuParam.show"
:x="contextMenuParam.x"
:y="contextMenuParam.y"
placement="bottom-start"
trigger="manual"
@clickoutside="contextMenuParam.show = false"
@select="handleSelectContextMenu"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,26 @@
<script setup>
const props = defineProps({
title: String,
})
</script>
<template>
<div class="db-tree-item flex-box-v">
<div class="tree-item-title">{{ title }}</div>
<div class="tree-item-addr">localhost:3306</div>
</div>
</template>
<style lang="scss">
.db-tree-item {
padding: 0 5px;
.tree-item-title {
font-size: 16px;
}
.tree-item-addr {
font-size: 12px;
}
}
</style>

View File

@ -0,0 +1,44 @@
<script setup>
import { NIcon } from 'naive-ui'
import AddGroup from '../icons/AddGroup.vue'
import AddLink from '../icons/AddLink.vue'
import DatabaseTree from './DatabaseTree.vue'
import IconButton from '../common/IconButton.vue'
import Filter from '../icons/Filter.vue'
import useTabStore from '../../stores/tab.js'
import { computed } from 'vue'
import { get } from 'lodash'
const tabStore = useTabStore()
const currentName = computed(() => get(tabStore.currentTab, 'name', ''))
</script>
<template>
<div class="nav-pane-container flex-box-v">
<database-tree :server="currentName" />
<!-- bottom function bar -->
<div class="nav-pane-bottom flex-box-h">
<icon-button :icon="AddLink" color="#555" size="20" stroke-width="4" t-tooltip="new_conn" />
<icon-button :icon="AddGroup" color="#555" size="20" stroke-width="4" t-tooltip="new_group" />
<n-input placeholder="">
<template #prefix>
<n-icon :component="Filter" color="#aaa" size="20" />
</template>
</n-input>
</div>
</div>
</template>
<style lang="scss" scoped>
.nav-pane-container {
overflow: hidden;
background-color: var(--bg-color);
.nav-pane-bottom {
align-items: center;
gap: 5px;
padding: 3px 3px 5px 5px;
}
}
</style>

View File

@ -1,38 +1,37 @@
<script setup>
import { h, nextTick, onMounted, reactive, ref } from 'vue'
import { ConnectionType } from '../consts/connection_type.js'
import useConnection from '../stores/connection.js'
import { ConnectionType } from '../../consts/connection_type.js'
import { NIcon, useDialog, useMessage } from 'naive-ui'
import ToggleFolder from './icons/ToggleFolder.vue'
import Key from './icons/Key.vue'
import ToggleDb from './icons/ToggleDb.vue'
import ToggleServer from './icons/ToggleServer.vue'
import Key from '../icons/Key.vue'
import ToggleDb from '../icons/ToggleDb.vue'
import { indexOf, remove, startsWith } from 'lodash'
import { useI18n } from 'vue-i18n'
import Refresh from './icons/Refresh.vue'
import Config from './icons/Config.vue'
import CopyLink from './icons/CopyLink.vue'
import Unlink from './icons/Unlink.vue'
import Add from './icons/Add.vue'
import Layer from './icons/Layer.vue'
import Delete from './icons/Delete.vue'
import Connect from './icons/Connect.vue'
import useDialogStore from '../stores/dialog.js'
import { ClipboardSetText } from '../../wailsjs/runtime/runtime.js'
import useTabStore from '../stores/tab.js'
import Refresh from '../icons/Refresh.vue'
import CopyLink from '../icons/CopyLink.vue'
import Add from '../icons/Add.vue'
import Layer from '../icons/Layer.vue'
import Delete from '../icons/Delete.vue'
import Connect from '../icons/Connect.vue'
import useDialogStore from '../../stores/dialog.js'
import { ClipboardSetText } from '../../../wailsjs/runtime/runtime.js'
import useTabStore from '../../stores/tab.js'
import useConnectionStore from '../../stores/connections.js'
const i18n = useI18n()
const loading = ref(false)
const loadingConnections = ref(false)
const expandedKeys = ref([])
const connectionStore = useConnection()
const connectionStore = useConnectionStore()
const tabStore = useTabStore()
const dialogStore = useDialogStore()
const showContextMenu = ref(false)
const contextPos = reactive({ x: 0, y: 0 })
const contextMenuOptions = ref(null)
const currentContextNode = ref(null)
const contextMenuParam = reactive({
show: false,
x: 0,
y: 0,
options: null,
currentNode: null,
})
const renderIcon = (icon) => {
return () => {
return h(NIcon, null, {
@ -41,61 +40,6 @@ const renderIcon = (icon) => {
}
}
const menuOptions = {
[ConnectionType.Group]: ({ opened }) => [
{
key: 'group_reload',
label: i18n.t('config_conn_group'),
icon: renderIcon(Config),
},
{
key: 'group_delete',
label: i18n.t('remove_conn_group'),
icon: renderIcon(Delete),
},
],
[ConnectionType.Server]: ({ connected }) => {
if (connected) {
return [
{
key: 'server_reload',
label: i18n.t('reload'),
icon: renderIcon(Refresh),
},
{
key: 'server_disconnect',
label: i18n.t('disconnect'),
icon: renderIcon(Unlink),
},
{
key: 'server_dup',
label: i18n.t('dup_conn'),
icon: renderIcon(CopyLink),
},
{
key: 'server_config',
label: i18n.t('config_conn'),
icon: renderIcon(Config),
},
{
type: 'divider',
key: 'd1',
},
{
key: 'server_remove',
label: i18n.t('remove_conn'),
icon: renderIcon(Delete),
},
]
} else {
return [
{
key: 'server_open',
label: i18n.t('open_connection'),
icon: renderIcon(Connect),
},
]
}
},
[ConnectionType.RedisDB]: ({ opened }) => {
if (opened) {
return [
@ -177,12 +121,16 @@ onMounted(async () => {
try {
// TODO: Show loading list status
loadingConnections.value = true
nextTick(connectionStore.initConnection)
// nextTick(connectionStore.initConnection)
} finally {
loadingConnections.value = false
}
})
const props = defineProps({
server: Strig,
})
const expandKey = (key) => {
const idx = indexOf(expandedKeys.value, key)
if (idx === -1) {
@ -224,22 +172,6 @@ const onUpdateExpanded = (value, option, meta) => {
const renderPrefix = ({ option }) => {
switch (option.type) {
case ConnectionType.Group:
return h(
NIcon,
{ size: 20 },
{
default: () => h(ToggleFolder, { modelValue: option.expanded === true }),
}
)
case ConnectionType.Server:
return h(
NIcon,
{ size: 20 },
{
default: () => h(ToggleServer, { modelValue: option.connected === true }),
}
)
case ConnectionType.RedisDB:
return h(
NIcon,
@ -302,6 +234,7 @@ const nodeProps = ({ option }) => {
case ConnectionType.RedisDB:
option.isLeaf = false
break
}
// default handle is expand current node
@ -313,13 +246,13 @@ const nodeProps = ({ option }) => {
if (mop == null) {
return
}
showContextMenu.value = false
contextMenuParam.show = false
nextTick().then(() => {
contextMenuOptions.value = mop(option)
currentContextNode.value = option
contextPos.x = e.clientX
contextPos.y = e.clientY
showContextMenu.value = true
contextMenuParam.options = mop(option)
contextMenuParam.currentNode = option
contextMenuParam.x = e.clientX
contextMenuParam.y = e.clientY
contextMenuParam.show = true
})
},
// onMouseover() {
@ -330,17 +263,6 @@ const nodeProps = ({ option }) => {
const onLoadTree = async (node) => {
switch (node.type) {
case ConnectionType.Server:
loading.value = true
try {
await connectionStore.openConnection(node.name)
} catch (e) {
message.error(e.message)
node.isLeaf = undefined
} finally {
loading.value = false
}
break
case ConnectionType.RedisDB:
loading.value = true
try {
@ -356,8 +278,8 @@ const onLoadTree = async (node) => {
}
const handleSelectContextMenu = (key) => {
showContextMenu.value = false
const { name, db, key: nodeKey, redisKey } = currentContextNode.value
contextMenuParam.show = false
const { name, db, key: nodeKey, redisKey } = contextMenuParam.currentNode
switch (key) {
case 'server_disconnect':
connectionStore.closeConnection(nodeKey).then((success) => {
@ -412,7 +334,7 @@ const handleSelectContextMenu = (key) => {
}
const handleOutsideContextMenu = () => {
showContextMenu.value = false
contextMenuParam.show = false
}
</script>
@ -420,7 +342,9 @@ const handleOutsideContextMenu = () => {
<n-tree
:block-line="true"
:block-node="true"
:data="connectionStore.connections"
:animated="false"
:cancelable="false"
:data="connectionStore.databases[props.server] || []"
:expand-on-click="false"
:expanded-keys="expandedKeys"
:node-props="nodeProps"
@ -429,17 +353,16 @@ const handleOutsideContextMenu = () => {
:render-label="renderLabel"
:render-prefix="renderPrefix"
:render-suffix="renderSuffix"
block-line
class="fill-height"
virtual-scroll
/>
<n-dropdown
:animated="false"
:options="contextMenuOptions"
:options="contextMenuParam.options"
:render-label="renderContextLabel"
:show="showContextMenu"
:x="contextPos.x"
:y="contextPos.y"
:show="contextMenuParam.show"
:x="contextMenuParam.x"
:y="contextMenuParam.y"
placement="bottom-start"
trigger="manual"
@clickoutside="handleOutsideContextMenu"

View File

@ -9,6 +9,9 @@
"new_group": "Add New Group",
"sort_conn": "Resort Connections",
"reload_key": "Reload Current Key",
"close_confirm_title": "Confirm Close",
"close_confirm": "Confirm close this tab and connection",
"opening_connection": "Opening Connection...",
"ttl": "TTL",
"forever": "Forever",
"rename_key": "Rename Key",
@ -31,8 +34,8 @@
"disconnect": "Disconnect",
"dup_conn": "Duplicate Connection",
"remove_conn": "Delete Connection",
"config_conn": "Edit Connection Config",
"config_conn_group": "Edit Connection Group",
"edit_conn": "Edit Connection Config",
"edit_conn_group": "Edit Connection Group",
"remove_conn_group": "Delete Connection Group",
"copy_path": "Copy Path",
"remove_path": "Remove Path",
@ -102,10 +105,12 @@
"field_required": "This item should not be blank",
"spec_field_required": "\"{key}\" should not be blank",
"no_connections": "No Connection",
"empty_tab_content": "Select the key from left list to see the details of the key.",
"empty_tab_content": "Select the key from left list to see detail.",
"empty_server_content": "Connect server from left list",
"reload_when_succ": "Reload immediately after success",
"server": "Server",
"structure": "Structure",
"log": "Log",
"about": "About",
"check_update": "Check for Updates..."
}

View File

@ -9,6 +9,9 @@
"new_group": "添加新分组",
"sort_conn": "调整连接顺序",
"reload_key": "重新载入此键内容",
"close_confirm_title": "关闭确认",
"close_confirm": "是否关闭当前连接",
"opening_connection": "正在打开连接...",
"ttl": "TTL",
"forever": "永久",
"rename_key": "重命名键",
@ -33,8 +36,8 @@
"disconnect": "断开连接",
"dup_conn": "复制连接",
"remove_conn": "删除连接",
"config_conn": "编辑连接配置",
"config_conn_group": "编辑连接分组",
"edit_conn": "编辑连接配置",
"edit_conn_group": "编辑连接分组",
"remove_conn_group": "删除连接分组",
"copy_path": "复制路径",
"remove_path": "删除路径",
@ -106,9 +109,11 @@
"spec_field_required": "{key} 不能为空",
"no_connections": "空空如也",
"empty_tab_content": "可以从左边选择键来查看键的详细内容",
"empty_server_content": "可以从左边选择并打开连接",
"reload_when_succ": "操作成功后立即重新加载",
"server": "服务器",
"structure": "结构",
"log": "日志",
"about": "关于",
"check_update": "检查更新..."
}

View File

@ -0,0 +1,921 @@
import { defineStore } from 'pinia'
import { get, isEmpty, last, remove, size, sortedIndexBy, split } from 'lodash'
import {
AddHashField,
AddListItem,
AddZSetValue,
CloseConnection,
GetKeyValue,
ListConnection,
OpenConnection,
OpenDatabase,
RemoveKey,
RenameKey,
SetHashValue,
SetKeyTTL,
SetKeyValue,
SetListItem,
SetSetItem,
UpdateSetItem,
UpdateZSetValue,
} from '../../wailsjs/go/services/connectionService.js'
import { ConnectionType } from '../consts/connection_type.js'
import useTabStore from './tab.js'
const separator = ':'
const useConnectionStore = defineStore('connections', {
/**
* @typedef {Object} ConnectionItem
* @property {string} key
* @property {string} label display label
* @property {string} name database name
* @property {number} type
* @property {ConnectionItem[]} children
*/
/**
* @typedef {Object} DatabaseItem
* @property {string} key
* @property {string} label
* @property {string} name - server name, type != ConnectionType.Group only
* @property {number} type
* @property {number} [db] - database index, type == ConnectionType.RedisDB only
* @property {number} keys
* @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
* @property {boolean} [expanded] - current node is expanded
*/
/**
*
* @returns {{databases: Object<string, DatabaseItem[]>, connections: ConnectionItem[]}}
*/
state: () => ({
connections: [], // all connections
databases: {}, // all databases in opened connections group by name
}),
getters: {
anyConnectionOpened() {
return !isEmpty(this.databases)
},
},
actions: {
/**
* Load all store connections struct from local profile
* @returns {Promise<void>}
*/
async initConnections() {
if (!isEmpty(this.connections)) {
return
}
const conns = []
const { data = [{ groupName: '', connections: [] }] } = await ListConnection()
for (let i = 0; i < data.length; i++) {
const group = data[i]
// Top level group
if (isEmpty(group.groupName)) {
const len = size(group.connections)
for (let j = 0; j < len; j++) {
const item = group.connections[j]
conns.push({
key: item.name,
label: item.name,
name: item.name,
type: ConnectionType.Server,
// isLeaf: false,
})
}
} else {
// Custom group
const children = []
const len = size(group.connections)
for (let j = 0; j < len; j++) {
const item = group.connections[j]
const value = group.groupName + '/' + item.name
children.push({
key: value,
label: item.name,
name: item.name,
type: ConnectionType.Server,
children: j === len - 1 ? undefined : [],
// isLeaf: false,
})
}
conns.push({
key: group.groupName,
label: group.groupName,
type: ConnectionType.Group,
children,
})
}
}
this.connections = conns
console.debug(JSON.stringify(this.connections))
},
/**
* get database server by name
* @param name
* @returns {ConnectionItem|null}
*/
getConnection(name) {
const conns = this.connections
for (let i = 0; i < conns.length; i++) {
if (conns[i].type === ConnectionType.Server && conns[i].key === name) {
return conns[i]
} else if (conns[i].type === ConnectionType.Group) {
const children = conns[i].children
for (let j = 0; j < children.length; j++) {
if (children[j].type === ConnectionType.Server && conns[i].key === name) {
return children[j]
}
}
}
}
return null
},
/**
* Check if connection is connected
* @param name
* @returns {boolean}
*/
isConnected(name) {
let dbs = get(this.databases, name, [])
return !isEmpty(dbs)
},
/**
* Open connection
* @param {string} name
* @returns {Promise<void>}
*/
async openConnection(name) {
if (this.isConnected(name)) {
return
}
const { data, success, msg } = await OpenConnection(name)
if (!success) {
throw new Error(msg)
}
// append to db node to current connection
// const connNode = this.getConnection(name)
// if (connNode == null) {
// throw new Error('no such connection')
// }
const { db } = data
if (isEmpty(db)) {
throw new Error('no db loaded')
}
const dbs = []
for (let i = 0; i < db.length; i++) {
dbs.push({
key: `${name}/${db[i].name}`,
label: db[i].name,
name: name,
keys: db[i].keys,
db: i,
type: ConnectionType.RedisDB,
isLeaf: false,
})
}
this.databases[name] = dbs
},
/**
* Close connection
* @param {string} name
* @returns {Promise<boolean>}
*/
async closeConnection(name) {
const { success, msg } = await CloseConnection(name)
if (!success) {
// throw new Error(msg)
return false
}
delete this.databases[name]
return true
},
/**
* Open database and load all keys
* @param connName
* @param db
* @returns {Promise<void>}
*/
async openDatabase(connName, db) {
const { data, success, msg } = await OpenDatabase(connName, db)
if (!success) {
throw new Error(msg)
}
const { keys = [] } = data
if (isEmpty(keys)) {
const dbs = this.databases[connName]
dbs[db].children = []
dbs[db].opened = true
return
}
// insert child to children list by order
const sortedInsertChild = (childrenList, item) => {
const insertIdx = sortedIndexBy(childrenList, item, 'key')
childrenList.splice(insertIdx, 0, item)
// childrenList.push(item)
}
// update all node item's children num
const updateChildrenNum = (node) => {
let count = 0
const totalChildren = size(node.children)
if (totalChildren > 0) {
for (const elem of node.children) {
updateChildrenNum(elem)
count += elem.keys
}
} else {
count += 1
}
node.keys = count
// node.children = sortBy(node.children, 'label')
}
const keyStruct = []
const mark = {}
for (const key in keys) {
const keyPart = split(key, separator)
// const prefixLen = size(keyPart) - 1
const len = size(keyPart)
let handlePath = ''
let ks = keyStruct
for (let i = 0; i < len; i++) {
handlePath += keyPart[i]
if (i !== len - 1) {
// layer
const treeKey = `${handlePath}@${ConnectionType.RedisKey}`
if (!mark.hasOwnProperty(treeKey)) {
mark[treeKey] = {
key: `${connName}/db${db}/${treeKey}`,
label: keyPart[i],
name: connName,
db,
keys: 0,
redisKey: handlePath,
type: ConnectionType.RedisKey,
children: [],
}
sortedInsertChild(ks, mark[treeKey])
}
ks = mark[treeKey].children
handlePath += separator
} else {
// key
const treeKey = `${handlePath}@${ConnectionType.RedisValue}`
mark[treeKey] = {
key: `${connName}/db${db}/${treeKey}`,
label: keyPart[i],
name: connName,
db,
keys: 0,
redisKey: handlePath,
type: ConnectionType.RedisValue,
}
sortedInsertChild(ks, mark[treeKey])
}
}
}
// append db node to current connection's children
const dbs = this.databases[connName]
dbs[db].children = keyStruct
dbs[db].opened = true
updateChildrenNum(dbs[db])
},
/**
* select node
* @param key
* @param name
* @param db
* @param type
* @param redisKey
*/
select({ key, name, db, type, redisKey }) {
if (type === ConnectionType.RedisValue) {
console.log(`[click]key:${key} db: ${db} redis key: ${redisKey}`)
// async get value for key
this.loadKeyValue(name, db, redisKey).then(() => {})
}
},
/**
* load redis key
* @param server
* @param db
* @param key
*/
async loadKeyValue(server, db, key) {
try {
const { data, success, msg } = await GetKeyValue(server, db, key)
if (success) {
const { type, ttl, value } = data
const tab = useTabStore()
tab.upsertTab({
server,
db,
type,
ttl,
key,
value,
})
} else {
console.warn('TODO: handle get key fail')
}
} finally {
}
},
/**
*
* @param {string} connName
* @param {number} db
* @param {string} key
* @private
*/
_addKey(connName, db, key) {
const dbs = this.databases[connName]
const dbDetail = get(dbs, db, {})
if (dbDetail == null) {
return
}
const descendantChain = [dbDetail]
const keyPart = split(key, separator)
let redisKey = ''
const keyLen = size(keyPart)
let added = false
for (let i = 0; i < keyLen; i++) {
redisKey += keyPart[i]
const node = last(descendantChain)
const nodeList = get(node, 'children', [])
const len = size(nodeList)
const isLastKeyPart = i === keyLen - 1
for (let j = 0; j < len + 1; j++) {
const treeKey = get(nodeList[j], 'key')
const isLast = j >= len - 1
const currentKey = `${connName}/db${db}/${redisKey}@${
isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey
}`
if (treeKey > currentKey || isLast) {
// out of search range, add new item
if (isLastKeyPart) {
// key not exists, add new one
const item = {
key: currentKey,
label: keyPart[i],
name: connName,
db,
keys: 1,
redisKey,
type: ConnectionType.RedisValue,
}
if (isLast) {
nodeList.push(item)
} else {
nodeList.splice(j, 0, item)
}
added = true
} else {
// layer not exists, add new one
const item = {
key: currentKey,
label: keyPart[i],
name: connName,
db,
keys: 0,
redisKey,
type: ConnectionType.RedisKey,
children: [],
}
if (isLast) {
nodeList.push(item)
descendantChain.push(last(nodeList))
} else {
nodeList.splice(j, 0, item)
descendantChain.push(nodeList[j])
}
redisKey += separator
added = true
}
break
} else if (treeKey === currentKey) {
if (isLastKeyPart) {
// same key exists, do nothing
console.log('TODO: same key exist, do nothing now, should replace value later')
} else {
// same group exists, find into it's children
descendantChain.push(nodeList[j])
redisKey += separator
}
break
}
}
}
// update ancestor node's info
if (added) {
const desLen = size(descendantChain)
for (let i = 0; i < desLen; i++) {
const children = get(descendantChain[i], 'children', [])
let keys = 0
for (const child of children) {
if (child.type === ConnectionType.RedisKey) {
keys += get(child, 'keys', 1)
} else if (child.type === ConnectionType.RedisValue) {
keys += get(child, 'keys', 0)
}
}
descendantChain[i].keys = keys
}
}
},
/**
* set redis key
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {number} keyType
* @param {any} value
* @param {number} ttl
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async setKey(connName, db, key, keyType, value, ttl) {
try {
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl)
if (success) {
// update tree view data
this._addKey(connName, db, key)
return { success }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* update hash field
* when field is set, newField is null, delete field
* when field is null, newField is set, add new field
* when both field and newField are set, and field === newField, update field
* when both field and newField are set, and field !== newField, delete field and add newField
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {string} field
* @param {string} newField
* @param {string} value
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
*/
async setHash(connName, db, key, field, newField, value) {
try {
const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '')
if (success) {
const { updated = {} } = data
return { success, updated }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* insert or update hash field item
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields
* @param {string[]} fieldItems field1, value1, filed2, value2...
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
*/
async addHashField(connName, db, key, action, fieldItems) {
try {
const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
if (success) {
const { updated = {} } = data
return { success, updated }
} else {
return { success: false, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* remove hash field
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {string} field
* @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
*/
async removeHashField(connName, db, key, field) {
try {
const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '')
if (success) {
const { removed = [] } = data
return { success, removed }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* insert list item
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {int} action 0: push to head, 1: push to tail
* @param {string[]}values
* @returns {Promise<*|{msg, success: boolean}>}
*/
async addListItem(connName, db, key, action, values) {
try {
return AddListItem(connName, db, key, action, values)
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* prepend item to head of list
* @param connName
* @param db
* @param key
* @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: []>}
*/
async prependListItem(connName, db, key, values) {
try {
const { data, success, msg } = await AddListItem(connName, db, key, 0, values)
if (success) {
const { left = [] } = data
return { success, item: left }
} else {
return { success: false, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* append item to tail of list
* @param connName
* @param db
* @param key
* @param values
* @returns {Promise<[msg]: string, success: boolean, [item]: any[]>}
*/
async appendListItem(connName, db, key, values) {
try {
const { data, success, msg } = await AddListItem(connName, db, key, 1, values)
if (success) {
const { right = [] } = data
return { success, item: right }
} else {
return { success: false, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* update value of list item by index
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {number} index
* @param {string} value
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
*/
async updateListItem(connName, db, key, index, value) {
try {
const { data, success, msg } = await SetListItem(connName, db, key, index, value)
if (success) {
const { updated = {} } = data
return { success, updated }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* remove list item
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {number} index
* @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>}
*/
async removeListItem(connName, db, key, index) {
try {
const { data, success, msg } = await SetListItem(connName, db, key, index, '')
if (success) {
const { removed = [] } = data
return { success, removed }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* add item to set
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {string} value
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async addSetItem(connName, db, key, value) {
try {
const { success, msg } = await SetSetItem(connName, db, key, false, [value])
if (success) {
return { success }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* update value of set item
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {string} value
* @param {string} newValue
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async updateSetItem(connName, db, key, value, newValue) {
try {
const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue)
if (success) {
return { success: true }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* remove item from set
* @param connName
* @param db
* @param key
* @param value
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async removeSetItem(connName, db, key, value) {
try {
const { success, msg } = await SetSetItem(connName, db, key, true, [value])
if (success) {
return { success }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* add item to sorted set
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {number} action
* @param {Object.<string, number>} vs value: score
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async addZSetItem(connName, db, key, action, vs) {
try {
const { success, msg } = await AddZSetValue(connName, db, key, action, vs)
if (success) {
return { success }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* update item of sorted set
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {string} value
* @param {string} newValue
* @param {number} score
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}, [removed]: []}>}
*/
async updateZSetItem(connName, db, key, value, newValue, score) {
try {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score)
if (success) {
const { updated, removed } = data
return { success, updated, removed }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* remove item from sorted set
* @param {string} connName
* @param {number} db
* @param key
* @param {string} value
* @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>}
*/
async removeZSetItem(connName, db, key, value) {
try {
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0)
if (success) {
const { removed } = data
return { success, removed }
} else {
return { success, msg }
}
} catch (e) {
return { success: false, msg: e.message }
}
},
/**
* reset key's ttl
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {number} ttl
* @returns {Promise<boolean>}
*/
async setTTL(connName, db, key, ttl) {
try {
const { success, msg } = await SetKeyTTL(connName, db, key, ttl)
return success === true
} catch (e) {
return false
}
},
/**
*
* @param {string} connName
* @param {number} db
* @param {string} key
* @private
*/
_removeKey(connName, db, key) {
const dbs = this.databases[connName]
const dbDetail = get(dbs, db, {})
if (dbDetail == null) {
return
}
const descendantChain = [dbDetail]
const keyPart = split(key, separator)
let redisKey = ''
const keyLen = size(keyPart)
let deleted = false
let forceBreak = false
for (let i = 0; i < keyLen && !forceBreak; i++) {
redisKey += keyPart[i]
const node = last(descendantChain)
const nodeList = get(node, 'children', [])
const len = size(nodeList)
const isLastKeyPart = i === keyLen - 1
for (let j = 0; j < len; j++) {
const treeKey = get(nodeList[j], 'key')
const currentKey = `${connName}/db${db}/${redisKey}@${
isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey
}`
if (treeKey > currentKey) {
// out of search range, target not exists
forceBreak = true
break
} else if (treeKey === currentKey) {
if (isLastKeyPart) {
// find target
nodeList.splice(j, 1)
node.keys -= 1
deleted = true
forceBreak = true
} else {
// find into it's children
descendantChain.push(nodeList[j])
redisKey += separator
}
break
}
}
if (forceBreak) {
break
}
}
// console.log(JSON.stringify(descendantChain))
// update ancestor node's info
if (deleted) {
const desLen = size(descendantChain)
for (let i = desLen - 1; i > 0; i--) {
const children = get(descendantChain[i], 'children', [])
const parent = descendantChain[i - 1]
if (isEmpty(children)) {
const parentChildren = get(parent, 'children', [])
const k = get(descendantChain[i], 'key')
remove(parentChildren, (item) => item.key === k)
}
parent.keys -= 1
}
}
},
/**
* remove redis key
* @param {string} connName
* @param {number} db
* @param {string} key
* @returns {Promise<boolean>}
*/
async removeKey(connName, db, key) {
try {
const { data, success, msg } = await RemoveKey(connName, db, key)
if (success) {
// update tree view data
this._removeKey(connName, db, key)
// set tab content empty
const tab = useTabStore()
tab.emptyTab(connName)
return true
}
} finally {
}
return false
},
/**
* rename key
* @param {string} connName
* @param {number} db
* @param {string} key
* @param {string} newKey
* @returns {Promise<{[msg]: string, success: boolean}>}
*/
async renameKey(connName, db, key, newKey) {
const { success = false, msg } = await RenameKey(connName, db, key, newKey)
if (success) {
// delete old key and add new key struct
this._removeKey(connName, db, key)
this._addKey(connName, db, newKey)
return { success: true }
} else {
return { success: false, msg }
}
},
},
})
export default useConnectionStore

View File

@ -6,7 +6,6 @@ import {
AddZSetValue,
CloseConnection,
GetKeyValue,
ListConnection,
OpenConnection,
OpenDatabase,
RemoveKey,
@ -21,12 +20,13 @@ import {
} from '../../wailsjs/go/services/connectionService.js'
import { ConnectionType } from '../consts/connection_type.js'
import useTabStore from './tab.js'
import useConnectionStore from './connections.js'
const separator = ':'
const useConnectionStore = defineStore('connection', {
const useDatabaseStore = defineStore('database', {
/**
* @typedef {Object} ConnectionItem
* @typedef {Object} DatabaseItem
* @property {string} key
* @property {string} label
* @property {string} name - server name, type != ConnectionType.Group only
@ -40,62 +40,14 @@ const useConnectionStore = defineStore('connection', {
/**
*
* @returns {{connections: ConnectionItem[]}}
* @returns {{connections: DatabaseItem[]}}
*/
state: () => ({
connections: [], // all connections list
databases: {}, // all database group by opened connections
}),
getters: {},
actions: {
/**
* Load all store connections struct from local profile
* @returns {Promise<void>}
*/
async initConnection() {
if (!isEmpty(this.connections)) {
return
}
const { data = [{ groupName: '', connections: [] }] } = await ListConnection()
for (let i = 0; i < data.length; i++) {
const group = data[i]
// Top level group
if (isEmpty(group.groupName)) {
for (let j = 0; j < group.connections.length; j++) {
const item = group.connections[j]
this.connections.push({
key: item.name,
label: item.name,
name: item.name,
type: ConnectionType.Server,
// isLeaf: false,
})
}
} else {
// Custom group
const children = []
for (let j = 0; j < group.connections.length; j++) {
const item = group.connections[j]
const value = group.groupName + '/' + item.name
children.push({
key: value,
label: item.name,
name: item.name,
type: ConnectionType.Server,
children: j === group.connections.length - 1 ? undefined : [],
// isLeaf: false,
})
}
this.connections.push({
key: group.groupName,
label: group.groupName,
type: ConnectionType.Group,
children,
})
}
}
console.debug(JSON.stringify(this.connections))
},
/**
* Open connection
* @param {string} connName
@ -107,7 +59,8 @@ const useConnectionStore = defineStore('connection', {
throw new Error(msg)
}
// append to db node to current connection
const connNode = this.getConnection(connName)
const connStore = useConnectionStore()
const connNode = connStore.getConnection(connName)
if (connNode == null) {
throw new Error('no such connection')
}
@ -901,4 +854,4 @@ const useConnectionStore = defineStore('connection', {
},
})
export default useConnectionStore
export default useDatabaseStore

View File

@ -1,4 +1,4 @@
import { find, findIndex, isEmpty, size } from 'lodash'
import { find, findIndex, size } from 'lodash'
import { defineStore } from 'pinia'
const useTabStore = defineStore('tab', {
@ -21,6 +21,8 @@ const useTabStore = defineStore('tab', {
* @returns {{tabList: TabItem[], activatedTab: string, activatedIndex: number}}
*/
state: () => ({
nav: 'server',
asideWidth: 300,
tabList: [],
activatedTab: '',
activatedIndex: 0, // current activated tab index
@ -31,9 +33,9 @@ const useTabStore = defineStore('tab', {
* @returns {TabItem[]}
*/
tabs() {
if (isEmpty(this.tabList)) {
this.newBlankTab()
}
// if (isEmpty(this.tabList)) {
// this.newBlankTab()
// }
return this.tabList
},
@ -53,7 +55,7 @@ const useTabStore = defineStore('tab', {
currentTabIndex() {
const len = size(this.tabs)
if (this.activatedIndex < 0 || this.activatedIndex >= len) {
this.activatedIndex = 0
this._setActivatedIndex(0)
}
return this.tabs[this.activatedIndex]
},
@ -68,17 +70,22 @@ const useTabStore = defineStore('tab', {
title: 'new tab',
blank: true,
})
this.activatedIndex = size(this.tabList) - 1
this._setActivatedIndex(size(this.tabList) - 1)
},
_setActivatedIndex(idx) {
this.activatedIndex = idx
this.nav = idx >= 0 ? 'structure' : 'server'
},
/**
* update or insert a new tab if not exists with the same name
* @param {string} server
* @param {number} db
* @param {number} type
* @param {number} ttl
* @param {string} key
* @param {*} value
* @param {number} [db]
* @param {number} [type]
* @param {number} [ttl]
* @param {string} [key]
* @param {*} [value]
*/
upsertTab({ server, db, type, ttl, key, value }) {
let tabIndex = findIndex(this.tabList, { name: server })
@ -90,20 +97,21 @@ const useTabStore = defineStore('tab', {
type,
ttl,
key,
value,
valu,
})
tabIndex = this.tabList.length - 1
}
const tab = this.tabList[tabIndex]
tab.blank = false
tab.title = `${server}/db${db}`
// 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.value = value
this.activatedIndex = tabIndex
this._setActivatedIndex(tabIndex)
// this.activatedTab = tab.name
},
@ -149,23 +157,27 @@ const useTabStore = defineStore('tab', {
const len = size(this.tabs)
// ignore remove last blank tab
if (len === 1 && this.tabs[0].blank) {
return
return null
}
if (tabIndex < 0 || tabIndex >= len) {
return
return null
}
this.tabList.splice(tabIndex, 1)
const removed = this.tabList.splice(tabIndex, 1)
if (len === 1) {
this.newBlankTab()
} else {
// Update select index if removed index equal current selected
this.activatedIndex -= 1
if (this.activatedIndex < 0 && this.tabList.length > 0) {
this.activatedIndex = 0
// update select index if removed index equal current selected
this.activatedIndex -= 1
if (this.activatedIndex < 0) {
if (this.tabList.length > 0) {
this._setActivatedIndex(0)
} else {
this._setActivatedIndex(-1)
}
} else {
this._setActivatedIndex(this.activatedIndex)
}
return size(removed) > 0 ? removed[0] : null
},
removeTabByName(tabName) {
const idx = findIndex(this.tabs, { name: tabName })

View File

@ -104,6 +104,6 @@ body {
}
.context-menu-item {
min-width: 120px;
min-width: 100px;
padding-right: 10px;
}