443 lines
15 KiB
Vue
443 lines
15 KiB
Vue
<script setup>
|
|
import { useThemeVars } from 'naive-ui'
|
|
import BrowserTree from './BrowserTree.vue'
|
|
import IconButton from '@/components/common/IconButton.vue'
|
|
import useTabStore from 'stores/tab.js'
|
|
import { computed, nextTick, onMounted, reactive, ref, unref, watch } from 'vue'
|
|
import { find, get, map, size } from 'lodash'
|
|
import Refresh from '@/components/icons/Refresh.vue'
|
|
import useDialogStore from 'stores/dialog.js'
|
|
import { useI18n } from 'vue-i18n'
|
|
import Search from '@/components/icons/Search.vue'
|
|
import Unlink from '@/components/icons/Unlink.vue'
|
|
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
|
|
import LoadAll from '@/components/icons/LoadAll.vue'
|
|
import LoadList from '@/components/icons/LoadList.vue'
|
|
import Delete from '@/components/icons/Delete.vue'
|
|
import useBrowserStore from 'stores/browser.js'
|
|
import { useRender } from '@/utils/render.js'
|
|
import RedisTypeSelector from '@/components/common/RedisTypeSelector.vue'
|
|
import { types } from '@/consts/support_redis_type.js'
|
|
import Plus from '@/components/icons/Plus.vue'
|
|
import useConnectionStore from 'stores/connections.js'
|
|
import Close from '@/components/icons/Close.vue'
|
|
import More from '@/components/icons/More.vue'
|
|
import Export from '@/components/icons/Export.vue'
|
|
import { ConnectionType } from '@/consts/connection_type.js'
|
|
import Import from '@/components/icons/Import.vue'
|
|
import Down from '@/components/icons/Down.vue'
|
|
import Checkbox from '@/components/icons/Checkbox.vue'
|
|
|
|
const props = defineProps({
|
|
server: String,
|
|
db: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
})
|
|
|
|
const themeVars = useThemeVars()
|
|
const i18n = useI18n()
|
|
const dialogStore = useDialogStore()
|
|
const tabStore = useTabStore()
|
|
const browserStore = useBrowserStore()
|
|
const connectionStore = useConnectionStore()
|
|
const render = useRender()
|
|
const browserTreeRef = ref(null)
|
|
const loading = ref(false)
|
|
const fullyLoaded = ref(false)
|
|
const inCheckState = ref(false)
|
|
|
|
const dbSelectOptions = computed(() => {
|
|
const dblist = browserStore.getDBList(props.server)
|
|
return map(dblist, (db) => {
|
|
if (props.db === db.db) {
|
|
return {
|
|
value: db.db,
|
|
label: `db${db.db} (${db.keyCount}/${db.maxKeys})`,
|
|
}
|
|
}
|
|
return {
|
|
value: db.db,
|
|
label: `db${db.db} (${db.maxKeys})`,
|
|
}
|
|
})
|
|
})
|
|
|
|
const addOptions = computed(() => [
|
|
{ key: 'import', label: i18n.t('interface.import_key'), icon: render.renderIcon(Import, { strokeWidth: 3.5 }) },
|
|
])
|
|
|
|
const moreOptions = computed(() => [
|
|
// { key: 'import', label: i18n.t('interface.import_key'), icon: render.renderIcon(Import, { strokeWidth: 3.5 }) },
|
|
{ key: 'flush', label: i18n.t('interface.flush_db'), icon: render.renderIcon(Delete, { strokeWidth: 3.5 }) },
|
|
{ key: 'divider', type: 'divider' },
|
|
{
|
|
key: 'disconnect',
|
|
label: i18n.t('interface.disconnect'),
|
|
icon: render.renderIcon(Unlink, { strokeWidth: 3.5 }),
|
|
},
|
|
])
|
|
|
|
const loadProgress = computed(() => {
|
|
const db = browserStore.getDatabase(props.server, props.db)
|
|
if (db.maxKeys <= 0) {
|
|
return 100
|
|
}
|
|
return (db.keyCount * 100) / Math.max(db.keyCount, db.maxKeys)
|
|
})
|
|
|
|
const checkedCount = computed(() => {
|
|
return size(tabStore.getCheckedKeys(props.server))
|
|
})
|
|
|
|
const checkedTip = computed(() => {
|
|
const dblist = browserStore.getDBList(props.server)
|
|
const db = find(dblist, { db: props.db })
|
|
return `${checkedCount.value} / ${Math.max(db.keyCount, checkedCount.value)}`
|
|
})
|
|
|
|
const onReload = async () => {
|
|
try {
|
|
loading.value = true
|
|
tabStore.setSelectedKeys(props.server)
|
|
const db = props.db
|
|
browserStore.closeDatabase(props.server, db)
|
|
// browserTreeRef.value?.resetExpandKey(props.server, db)
|
|
|
|
let matchType = unref(filterForm.type)
|
|
if (!types.hasOwnProperty(matchType)) {
|
|
matchType = ''
|
|
}
|
|
browserStore.setKeyFilter(props.server, {
|
|
type: matchType,
|
|
pattern: unref(filterForm.pattern),
|
|
})
|
|
await browserStore.openDatabase(props.server, db)
|
|
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db)
|
|
// $message.success(i18n.t('dialogue.reload_succ'))
|
|
} catch (e) {
|
|
console.warn(e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const onAddKey = () => {
|
|
const selectedKey = get(browserTreeRef.value?.getSelectedKey(), 0)
|
|
if (selectedKey != null) {
|
|
const node = browserStore.getNode(selectedKey)
|
|
const { type = ConnectionType.RedisValue, redisKey } = node
|
|
if (type === ConnectionType.RedisKey) {
|
|
// has prefix
|
|
dialogStore.openNewKeyDialog(redisKey, props.server, props.db)
|
|
return
|
|
}
|
|
}
|
|
dialogStore.openNewKeyDialog('', props.server, props.db)
|
|
}
|
|
|
|
const onLoadMore = async () => {
|
|
try {
|
|
loading.value = true
|
|
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, props.db)
|
|
} catch (e) {
|
|
$message.error(e.message)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const onLoadAll = async () => {
|
|
try {
|
|
loading.value = true
|
|
await browserStore.loadAllKeys(props.server, props.db)
|
|
fullyLoaded.value = true
|
|
} catch (e) {
|
|
$message.error(e.message)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const onDeleteChecked = () => {
|
|
browserTreeRef.value?.deleteCheckedItems()
|
|
}
|
|
|
|
const onExportChecked = () => {
|
|
browserTreeRef.value?.exportCheckedItems()
|
|
}
|
|
|
|
const onImportData = () => {
|
|
dialogStore.openImportKeyDialog(props.server, props.db)
|
|
}
|
|
|
|
const onFlush = () => {
|
|
dialogStore.openFlushDBDialog(props.server, props.db)
|
|
}
|
|
|
|
const onDisconnect = () => {
|
|
browserStore.closeConnection(props.server)
|
|
}
|
|
|
|
const handleSelectDB = async (db) => {
|
|
if (db === props.db) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
loading.value = true
|
|
browserStore.setKeyFilter(props.server, {})
|
|
browserStore.closeDatabase(props.server, props.db)
|
|
await browserStore.openDatabase(props.server, db)
|
|
await nextTick()
|
|
await connectionStore.saveLastDB(props.server, db)
|
|
tabStore.upsertTab({ server: props.server, db, clearValue: true })
|
|
// browserTreeRef.value?.resetExpandKey(props.server, db)
|
|
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db)
|
|
browserTreeRef.value?.refreshTree()
|
|
tabStore.setSelectedKeys(props.server)
|
|
} catch (e) {
|
|
$message.error(e.message)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const filterForm = reactive({
|
|
type: '',
|
|
pattern: '',
|
|
filter: '',
|
|
})
|
|
const onSelectFilterType = (select) => {
|
|
onReload()
|
|
}
|
|
|
|
const onFilterInput = (val) => {
|
|
filterForm.filter = val
|
|
}
|
|
|
|
const onMatchInput = (matchVal, filterVal) => {
|
|
filterForm.pattern = matchVal
|
|
filterForm.filter = filterVal
|
|
onReload()
|
|
}
|
|
|
|
const onSelectOptions = (select) => {
|
|
switch (select) {
|
|
case 'import':
|
|
onImportData()
|
|
break
|
|
case 'flush':
|
|
onFlush()
|
|
break
|
|
case 'disconnect':
|
|
onDisconnect()
|
|
break
|
|
}
|
|
}
|
|
|
|
onMounted(() => onReload())
|
|
|
|
watch(
|
|
() => browserStore.getReloadKey(props.server),
|
|
(key) => onReload(),
|
|
)
|
|
// forbid dynamic switch key view due to performance issues
|
|
// const viewType = ref(0)
|
|
// const onSwitchView = (selectView) => {
|
|
// const { server } = tabStore.currentTab
|
|
// browserStore.switchKeyView(server, selectView)
|
|
// }
|
|
</script>
|
|
|
|
<template>
|
|
<div class="nav-pane-container flex-box-v">
|
|
<!-- top function bar -->
|
|
<div class="flex-box-h nav-pane-func" style="height: 36px">
|
|
<content-search-input
|
|
:debounce-wait="1000"
|
|
:full-search-icon="Search"
|
|
small
|
|
use-glob
|
|
@filter-changed="onFilterInput"
|
|
@match-changed="onMatchInput">
|
|
<template #prepend>
|
|
<redis-type-selector v-model:value="filterForm.type" @update:value="onSelectFilterType" />
|
|
</template>
|
|
</content-search-input>
|
|
<icon-button
|
|
:disabled="loading"
|
|
:icon="Refresh"
|
|
border
|
|
size="18"
|
|
small
|
|
stroke-width="4"
|
|
t-tooltip="interface.reload"
|
|
@click="onReload" />
|
|
<n-button-group>
|
|
<icon-button
|
|
:disabled="loading"
|
|
:icon="Plus"
|
|
border
|
|
size="18"
|
|
small
|
|
stroke-width="4"
|
|
t-tooltip="interface.new_key"
|
|
@click="onAddKey" />
|
|
<n-dropdown
|
|
:options="addOptions"
|
|
placement="bottom-end"
|
|
style="min-width: 130px"
|
|
trigger="click"
|
|
@select="onSelectOptions">
|
|
<n-button :focusable="false" size="small" style="padding: 0 3px">
|
|
<n-icon size="10">
|
|
<down :stroke-width="6" />
|
|
</n-icon>
|
|
</n-button>
|
|
</n-dropdown>
|
|
</n-button-group>
|
|
</div>
|
|
|
|
<!-- loaded progress -->
|
|
<n-progress
|
|
:border-radius="0"
|
|
:color="loadProgress >= 100 ? '#0000' : themeVars.primaryColor"
|
|
:height="2"
|
|
:percentage="loadProgress"
|
|
:processing="loading"
|
|
:show-indicator="false"
|
|
status="success"
|
|
type="line" />
|
|
|
|
<!-- tree view -->
|
|
<browser-tree
|
|
ref="browserTreeRef"
|
|
:check-mode="inCheckState"
|
|
:db="props.db"
|
|
:full-loaded="fullyLoaded"
|
|
:loading="loading && loadProgress <= 0"
|
|
:pattern="filterForm.filter"
|
|
:server="props.server" />
|
|
<!-- bottom function bar -->
|
|
<div class="nav-pane-bottom flex-box-v">
|
|
<!-- <switch-button-->
|
|
<!-- v-model:value="viewType"-->
|
|
<!-- :icons="[TreeView, ListView]"-->
|
|
<!-- :t-tooltips="['interface.tree_view', 'interface.list_view']"-->
|
|
<!-- :stroke-width="3.5"-->
|
|
<!-- unselect-stroke-width="3"-->
|
|
<!-- @update:value="onSwitchView" />-->
|
|
<transition mode="out-in" name="fade">
|
|
<div v-if="!inCheckState" class="flex-box-h nav-pane-func">
|
|
<n-select
|
|
:consistent-menu-width="false"
|
|
:filter="(pattern, option) => option.value.toString() === pattern"
|
|
:options="dbSelectOptions"
|
|
:value="props.db"
|
|
filterable
|
|
size="small"
|
|
style="min-width: 100px; max-width: 200px"
|
|
@update:value="handleSelectDB" />
|
|
<icon-button
|
|
:button-class="['nav-pane-func-btn']"
|
|
:disabled="fullyLoaded"
|
|
:icon="LoadList"
|
|
:loading="loading"
|
|
:stroke-width="3.5"
|
|
size="20"
|
|
t-tooltip="interface.load_more"
|
|
@click="onLoadMore" />
|
|
<icon-button
|
|
:button-class="['nav-pane-func-btn']"
|
|
:disabled="fullyLoaded"
|
|
:icon="LoadAll"
|
|
:loading="loading"
|
|
:stroke-width="3.5"
|
|
size="20"
|
|
t-tooltip="interface.load_all"
|
|
@click="onLoadAll" />
|
|
<div class="flex-item-expand" style="min-width: 10px" />
|
|
<icon-button
|
|
:button-class="['nav-pane-func-btn']"
|
|
:icon="Checkbox"
|
|
:stroke-width="3.5"
|
|
size="20"
|
|
t-tooltip="interface.check_mode"
|
|
@click="inCheckState = true" />
|
|
<n-dropdown
|
|
:options="moreOptions"
|
|
placement="top-end"
|
|
style="min-width: 130px"
|
|
@select="onSelectOptions">
|
|
<icon-button :button-class="['nav-pane-func-btn']" :icon="More" :stroke-width="3.5" size="20" />
|
|
</n-dropdown>
|
|
</div>
|
|
|
|
<!-- check mode function bar -->
|
|
<div v-else class="flex-box-h nav-pane-func">
|
|
<icon-button
|
|
:button-class="['nav-pane-func-btn']"
|
|
:disabled="checkedCount <= 0"
|
|
:icon="Export"
|
|
:stroke-width="3.5"
|
|
size="20"
|
|
t-tooltip="interface.export_checked"
|
|
@click="onExportChecked" />
|
|
<icon-button
|
|
:button-class="['nav-pane-func-btn']"
|
|
:disabled="checkedCount <= 0"
|
|
:icon="Delete"
|
|
:stroke-width="3.5"
|
|
size="20"
|
|
t-tooltip="interface.delete_checked"
|
|
@click="onDeleteChecked" />
|
|
<div class="flex-item-expand ellipsis" style="text-align: center; margin: 0 5px">
|
|
<n-text>{{ checkedTip }}</n-text>
|
|
</div>
|
|
<icon-button
|
|
:button-class="['nav-pane-func-btn']"
|
|
:icon="Close"
|
|
:stroke-width="3.5"
|
|
size="20"
|
|
t-tooltip="interface.quit_check_mode"
|
|
@click="inCheckState = false" />
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
@import '@/styles/style';
|
|
|
|
:deep(.toggle-btn) {
|
|
border-style: solid;
|
|
border-width: 1px;
|
|
border-radius: 3px;
|
|
padding: 4px;
|
|
}
|
|
|
|
:deep(.toggle-on) {
|
|
border-color: v-bind('themeVars.iconColorDisabled');
|
|
background-color: v-bind('themeVars.iconColorDisabled');
|
|
}
|
|
|
|
:deep(.toggle-off) {
|
|
border-color: #0000;
|
|
}
|
|
|
|
.nav-pane-top {
|
|
//@include bottom-shadow(0.1);
|
|
color: v-bind('themeVars.iconColor');
|
|
border-bottom: v-bind('themeVars.borderColor') 1px solid;
|
|
}
|
|
|
|
.nav-pane-bottom {
|
|
@include top-shadow(0.1);
|
|
color: v-bind('themeVars.iconColor');
|
|
border-top: v-bind('themeVars.borderColor') 1px solid;
|
|
}
|
|
</style>
|