tiny-rdm/frontend/src/components/sidebar/ConnectionTree.vue

548 lines
16 KiB
Vue
Raw Normal View History

<script setup>
2023-08-02 17:57:39 +08:00
import useDialogStore from 'stores/dialog.js'
2023-07-18 17:35:31 +08:00
import { h, nextTick, reactive, ref } from 'vue'
2023-08-02 17:57:39 +08:00
import useConnectionStore from 'stores/connections.js'
import { NIcon, NSpace, NText, useThemeVars } from 'naive-ui'
2023-08-02 17:57:39 +08:00
import { ConnectionType } from '@/consts/connection_type.js'
import ToggleFolder from '@/components/icons/ToggleFolder.vue'
import ToggleServer from '@/components/icons/ToggleServer.vue'
2023-10-14 21:26:47 +08:00
import ToggleCluster from '@/components/icons/ToggleCluster.vue'
import { debounce, get, includes, indexOf, isEmpty, split } from 'lodash'
2023-08-02 17:57:39 +08:00
import Config from '@/components/icons/Config.vue'
import Delete from '@/components/icons/Delete.vue'
import Unlink from '@/components/icons/Unlink.vue'
import CopyLink from '@/components/icons/CopyLink.vue'
import Connect from '@/components/icons/Connect.vue'
import { useI18n } from 'vue-i18n'
2023-08-02 17:57:39 +08:00
import useTabStore from 'stores/tab.js'
import Edit from '@/components/icons/Edit.vue'
import { hexGammaCorrection, parseHexColor, toHexColor } from '@/utils/rgb.js'
import IconButton from '@/components/common/IconButton.vue'
import usePreferencesStore from 'stores/preferences.js'
2023-07-16 01:50:01 +08:00
const themeVars = useThemeVars()
const i18n = useI18n()
const connectingServer = ref('')
const connectionStore = useConnectionStore()
const tabStore = useTabStore()
const prefStore = usePreferencesStore()
const dialogStore = useDialogStore()
const expandedKeys = ref([])
const selectedKeys = ref([])
const props = defineProps({
filterPattern: {
type: String,
},
})
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_rename',
label: i18n.t('interface.rename_conn_group'),
icon: renderIcon(Edit),
},
{
key: 'group_delete',
label: i18n.t('interface.remove_conn_group'),
icon: renderIcon(Delete),
},
],
[ConnectionType.Server]: ({ name }) => {
const connected = connectionStore.isConnected(name)
if (connected) {
return [
{
key: 'server_close',
label: i18n.t('interface.disconnect'),
icon: renderIcon(Unlink),
},
{
key: 'server_edit',
label: i18n.t('interface.edit_conn'),
icon: renderIcon(Config),
},
{
key: 'server_dup',
label: i18n.t('interface.dup_conn'),
icon: renderIcon(CopyLink),
},
{
type: 'divider',
key: 'd1',
},
{
key: 'server_remove',
label: i18n.t('interface.remove_conn'),
icon: renderIcon(Delete),
},
]
} else {
return [
{
key: 'server_open',
label: i18n.t('interface.open_connection'),
icon: renderIcon(Connect),
},
{
key: 'server_edit',
label: i18n.t('interface.edit_conn'),
2023-08-25 16:08:04 +08:00
icon: renderIcon(Config),
},
{
key: 'server_dup',
label: i18n.t('interface.dup_conn'),
icon: renderIcon(CopyLink),
},
{
type: 'divider',
key: 'd1',
},
{
key: 'server_remove',
label: i18n.t('interface.remove_conn'),
icon: renderIcon(Delete),
},
]
}
},
}
/**
* get mark color of server saved in preferences
* @param name
* @return {null|string}
*/
const getServerMarkColor = (name) => {
const { markColor = '' } = connectionStore.serverProfile[name] || {}
if (!isEmpty(markColor)) {
const rgb = parseHexColor(markColor)
const rgb2 = hexGammaCorrection(rgb, 0.75)
return toHexColor(rgb2)
}
return null
}
const renderLabel = ({ option }) => {
if (option.type === ConnectionType.Server) {
const color = getServerMarkColor(option.name)
if (color != null) {
return h(
NText,
{
2023-08-25 16:08:04 +08:00
style: {
color,
fontWeight: '450',
},
},
() => option.label,
)
}
}
return option.label
}
// render horizontal item
const renderIconMenu = (items) => {
return h(
NSpace,
{
align: 'center',
inline: true,
size: 3,
wrapItem: false,
wrap: false,
style: 'margin-right: 5px',
},
() => items,
)
}
const renderPrefix = ({ option }) => {
const iconTransparency = prefStore.isDark ? 0.75 : 1
switch (option.type) {
case ConnectionType.Group:
const opened = indexOf(expandedKeys.value, option.key) !== -1
return h(
NIcon,
{ size: 20 },
{
default: () =>
h(ToggleFolder, {
modelValue: opened,
fillColor: `rgba(255,206,120,${iconTransparency})`,
}),
2023-08-02 17:57:39 +08:00
},
)
case ConnectionType.Server:
const connected = connectionStore.isConnected(option.name)
const color = getServerMarkColor(option.name)
2023-10-14 21:26:47 +08:00
const icon = option.cluster === true ? ToggleCluster : ToggleServer
return h(
NIcon,
{ size: 20, color: !!!connected ? color : undefined },
{
default: () =>
2023-10-14 21:26:47 +08:00
h(icon, {
modelValue: !!connected,
fillColor: `rgba(220,66,60,${iconTransparency})`,
}),
2023-08-02 17:57:39 +08:00
},
)
}
}
const getServerMenu = (connected) => {
const btns = []
if (connected) {
btns.push(
h(IconButton, {
tTooltip: 'interface.disconnect',
icon: Unlink,
onClick: () => handleSelectContextMenu('server_close'),
}),
h(IconButton, {
tTooltip: 'interface.edit_conn',
icon: Config,
onClick: () => handleSelectContextMenu('server_edit'),
}),
)
} else {
btns.push(
h(IconButton, {
tTooltip: 'interface.open_connection',
icon: Connect,
onClick: () => handleSelectContextMenu('server_open'),
}),
h(IconButton, {
tTooltip: 'interface.edit_conn',
icon: Config,
onClick: () => handleSelectContextMenu('server_edit'),
}),
h(IconButton, {
tTooltip: 'interface.remove_conn',
icon: Delete,
onClick: () => handleSelectContextMenu('server_remove'),
}),
)
}
return btns
}
const getGroupMenu = () => {
return [
h(IconButton, {
tTooltip: 'interface.edit_conn',
icon: Config,
onClick: () => handleSelectContextMenu('group_rename'),
}),
h(IconButton, {
tTooltip: 'interface.remove_conn',
icon: Delete,
onClick: () => handleSelectContextMenu('group_delete'),
}),
]
}
2023-07-18 17:35:31 +08:00
const renderSuffix = ({ option }) => {
if (includes(selectedKeys.value, option.key)) {
switch (option.type) {
case ConnectionType.Server:
const connected = connectionStore.isConnected(option.name)
return renderIconMenu(getServerMenu(connected))
case ConnectionType.Group:
return renderIconMenu(getGroupMenu())
2023-07-18 17:35:31 +08:00
}
}
return null
}
const onUpdateExpandedKeys = (keys, option) => {
expandedKeys.value = keys
}
const onUpdateSelectedKeys = (keys, option) => {
selectedKeys.value = keys
}
/**
* Open connection
* @param name
* @returns {Promise<void>}
*/
const openConnection = async (name) => {
try {
connectingServer.value = name
if (!connectionStore.isConnected(name)) {
await connectionStore.openConnection(name)
}
// check if connection already canceled before finish open
if (!isEmpty(connectingServer.value)) {
tabStore.upsertTab({
server: name,
})
}
} catch (e) {
$message.error(e.message)
// node.isLeaf = undefined
} finally {
connectingServer.value = ''
}
}
const removeConnection = (name) => {
$dialog.warning(
i18n.t('dialogue.remove_tip', { type: i18n.t('dialogue.connection.conn_name'), name }),
async () => {
connectionStore.deleteConnection(name).then(({ success, msg }) => {
if (!success) {
$message.error(msg)
}
})
},
)
}
const removeGroup = async (name) => {
$dialog.warning(i18n.t('dialogue.remove_group_tip', { name }), async () => {
connectionStore.deleteGroup(name).then(({ success, msg }) => {
if (!success) {
$message.error(msg)
}
})
})
}
const expandKey = (key) => {
const idx = indexOf(expandedKeys.value, key)
if (idx === -1) {
expandedKeys.value.push(key)
} else {
expandedKeys.value.splice(idx, 1)
}
}
const nodeProps = ({ option }) => {
return {
onDblclick: async () => {
if (option.type === ConnectionType.Server) {
openConnection(option.name).then(() => {})
} else if (option.type === ConnectionType.Group) {
// toggle expand
nextTick().then(() => expandKey(option.key))
}
},
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
selectedKeys.value = [option.key]
})
},
}
}
const renderContextLabel = (option) => {
return h('div', { class: 'context-menu-item' }, option.label)
}
const handleSelectContextMenu = (key) => {
contextMenuParam.show = false
const selectedKey = get(selectedKeys.value, 0)
if (selectedKey == null) {
return
}
const [group, name] = split(selectedKey, '/')
if (isEmpty(group) && isEmpty(name)) {
return
}
switch (key) {
case 'server_open':
openConnection(name).then(() => {})
break
case 'server_edit':
// ask for close relevant connections before edit
if (connectionStore.isConnected(name)) {
$dialog.warning(i18n.t('dialogue.edit_close_confirm'), () => {
connectionStore.closeConnection(name)
dialogStore.openEditDialog(name)
})
} else {
dialogStore.openEditDialog(name)
}
break
case 'server_dup':
dialogStore.openDuplicateDialog(name)
break
case 'server_remove':
removeConnection(name)
break
case 'server_close':
connectionStore.closeConnection(name).then((closed) => {
if (closed) {
$message.success(i18n.t('dialogue.handle_succ'))
}
})
break
case 'group_rename':
if (!isEmpty(group)) {
dialogStore.openRenameGroupDialog(group)
}
break
case 'group_delete':
if (!isEmpty(group)) {
removeGroup(group)
}
break
default:
console.warn('TODO: handle context menu:' + key)
}
}
const findSiblingsAndIndex = (node, nodes) => {
if (!nodes) {
return [null, null]
}
for (let i = 0; i < nodes.length; ++i) {
const siblingNode = nodes[i]
if (siblingNode.key === node.key) {
return [nodes, i]
}
const [siblings, index] = findSiblingsAndIndex(node, siblingNode.children)
if (siblings && index !== null) {
return [siblings, index]
}
}
return [null, null]
}
// delay save until drop stopped after 2 seconds
const saveSort = debounce(connectionStore.saveConnectionSorted, 2000, { trailing: true })
const handleDrop = ({ node, dragNode, dropPosition }) => {
const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)
if (dragNodeSiblings === null || dragNodeIndex === null) {
return
}
dragNodeSiblings.splice(dragNodeIndex, 1)
if (dropPosition === 'inside') {
if (node.children) {
node.children.unshift(dragNode)
} else {
node.children = [dragNode]
}
} else if (dropPosition === 'before') {
const [nodeSiblings, nodeIndex] = findSiblingsAndIndex(node, connectionStore.connections)
if (nodeSiblings === null || nodeIndex === null) {
return
}
nodeSiblings.splice(nodeIndex, 0, dragNode)
} else if (dropPosition === 'after') {
const [nodeSiblings, nodeIndex] = findSiblingsAndIndex(node, connectionStore.connections)
if (nodeSiblings === null || nodeIndex === null) {
return
}
nodeSiblings.splice(nodeIndex + 1, 0, dragNode)
}
connectionStore.connections = Array.from(connectionStore.connections)
saveSort()
}
const onCancelOpen = () => {
if (!isEmpty(connectingServer.value)) {
connectionStore.closeConnection(connectingServer.value)
connectingServer.value = ''
}
}
</script>
<template>
<n-empty
v-if="isEmpty(connectionStore.connections)"
:description="$t('interface.empty_server_list')"
class="empty-content" />
<n-tree
2023-08-02 17:57:39 +08:00
v-else
:animated="false"
:block-line="true"
:block-node="true"
:cancelable="false"
:data="connectionStore.connections"
2023-08-02 19:40:57 +08:00
:draggable="true"
:expanded-keys="expandedKeys"
:node-props="nodeProps"
2023-08-02 19:40:57 +08:00
:pattern="props.filterPattern"
:render-label="renderLabel"
:render-prefix="renderPrefix"
2023-07-18 17:35:31 +08:00
:render-suffix="renderSuffix"
2023-08-02 19:40:57 +08:00
:selected-keys="selectedKeys"
class="fill-height"
virtual-scroll
2023-08-02 19:40:57 +08:00
@drop="handleDrop"
@update:selected-keys="onUpdateSelectedKeys"
2023-08-24 15:23:25 +08:00
@update:expanded-keys="onUpdateExpandedKeys" />
<!-- status display modal -->
<n-modal :show="connectingServer !== ''" transform-origin="center">
<n-card
:bordered="false"
:content-style="{ textAlign: 'center' }"
aria-model="true"
role="dialog"
2023-08-24 15:23:25 +08:00
style="width: 400px">
<n-spin>
<template #description>
<n-space vertical>
<n-text strong>{{ $t('dialogue.opening_connection') }}</n-text>
<n-button :focusable="false" secondary size="small" @click="onCancelOpen">
{{ $t('dialogue.interrupt_connection') }}
</n-button>
</n-space>
</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"
2023-08-24 15:23:25 +08:00
@select="handleSelectContextMenu" />
</template>
2023-08-02 17:57:39 +08:00
<style lang="scss" scoped>
@import '@/styles/content';
</style>