diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index 8369bf9..e83f8e4 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -390,7 +390,20 @@ func (b *browserService) ServerInfo(name string) (resp types.JSResp) { // @param path contain connection name and db name func (b *browserService) OpenDatabase(connName string, db int, match string, keyType string) (resp types.JSResp) { b.setClientCursor(connName, db, 0) - return b.LoadNextKeys(connName, db, match, keyType) + + item, err := b.getRedisClient(connName, db) + if err != nil { + resp.Msg = err.Error() + return + } + client, ctx := item.client, item.ctx + maxKeys := b.loadDBSize(ctx, client) + + resp.Success = true + resp.Data = map[string]any{ + "maxKeys": maxKeys, + } + return } // scan keys @@ -433,6 +446,7 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli // cluster mode var mutex sync.Mutex err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error { + // FIXME: BUG? can not fully load in cluster mode? maybe remove the shared "cursor" return scan(ctx, cli, func(k []any) { mutex.Lock() keys = append(keys, k...) diff --git a/frontend/src/components/common/IconButton.vue b/frontend/src/components/common/IconButton.vue index e632d46..4692da5 100644 --- a/frontend/src/components/common/IconButton.vue +++ b/frontend/src/components/common/IconButton.vue @@ -26,6 +26,7 @@ const props = defineProps({ disabled: Boolean, buttonStyle: [String, Object], buttonClass: [String, Object], + small: Boolean, }) const hasTooltip = computed(() => { @@ -42,6 +43,7 @@ const hasTooltip = computed(() => { :disabled="disabled" :focusable="false" :loading="loading" + :size="small ? 'small' : ''" :style="props.buttonStyle" :text="!border" :type="type" @@ -64,6 +66,7 @@ const hasTooltip = computed(() => { :disabled="disabled" :focusable="false" :loading="loading" + :size="small ? 'small' : ''" :style="props.buttonStyle" :text="!border" :type="type" diff --git a/frontend/src/components/common/RedisTypeSelector.vue b/frontend/src/components/common/RedisTypeSelector.vue new file mode 100644 index 0000000..c6abe5d --- /dev/null +++ b/frontend/src/components/common/RedisTypeSelector.vue @@ -0,0 +1,83 @@ + + + + + + {{ displayValue }} + + + + + diff --git a/frontend/src/components/common/RedisTypeTag.vue b/frontend/src/components/common/RedisTypeTag.vue index 3bba2de..eb9573d 100644 --- a/frontend/src/components/common/RedisTypeTag.vue +++ b/frontend/src/components/common/RedisTypeTag.vue @@ -12,7 +12,6 @@ const props = defineProps({ default: 'STRING', }, binaryKey: Boolean, - bordered: Boolean, size: String, }) @@ -27,9 +26,8 @@ const backgroundColor = computed(() => { {{ props.type }} diff --git a/frontend/src/components/content_value/ContentSearchInput.vue b/frontend/src/components/content_value/ContentSearchInput.vue index 9f5b958..3bba4ac 100644 --- a/frontend/src/components/content_value/ContentSearchInput.vue +++ b/frontend/src/components/content_value/ContentSearchInput.vue @@ -3,12 +3,21 @@ import { computed, reactive } from 'vue' import { debounce, isEmpty, trim } from 'lodash' import { NButton, NInput } from 'naive-ui' import IconButton from '@/components/common/IconButton.vue' +import Help from '@/components/icons/Help.vue' const props = defineProps({ fullSearchIcon: { type: [String, Object], default: null, }, + debounceWait: { + type: Number, + default: 500, + }, + small: { + type: Boolean, + default: false, + }, }) const emit = defineEmits(['filterChanged', 'matchChanged']) @@ -42,7 +51,13 @@ const onFullSearch = () => { const _onInput = () => { emit('filterChanged', inputData.filter) } -const onInput = debounce(_onInput, 500, { leading: true, trailing: true }) +const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true }) + +const onKeyup = (evt) => { + if (evt.key === 'Enter') { + onFullSearch() + } +} const onClearFilter = () => { inputData.filter = '' @@ -67,13 +82,17 @@ defineExpose({ + + @input="onInput" + @keyup.enter="onKeyup"> + @@ -83,12 +102,25 @@ defineExpose({ {{ $t('interface.full_search_result', { pattern: inputData.match }) }} + + + + + + + {{ $t('dialogue.filter.filter_pattern_tip') }} + + + + diff --git a/frontend/src/components/dialogs/KeyFilterDialog.vue b/frontend/src/components/dialogs/KeyFilterDialog.vue index 65382a0..38b4f76 100644 --- a/frontend/src/components/dialogs/KeyFilterDialog.vue +++ b/frontend/src/components/dialogs/KeyFilterDialog.vue @@ -40,11 +40,7 @@ watch( ) const browserStore = useBrowserStore() -const onConfirm = () => { - const { server, db, type, pattern } = filterForm - browserStore.setKeyFilter(server, db, pattern, type) - browserStore.reopenDatabase(server, db) -} +const onConfirm = () => {} const onClose = () => { dialogStore.closeKeyFilterDialog() diff --git a/frontend/src/components/dialogs/NewKeyDialog.vue b/frontend/src/components/dialogs/NewKeyDialog.vue index d46d784..4102c17 100644 --- a/frontend/src/components/dialogs/NewKeyDialog.vue +++ b/frontend/src/components/dialogs/NewKeyDialog.vue @@ -109,14 +109,16 @@ const onAdd = async () => { $message.error(err) } }) - await subFormRef.value?.validate((errs) => { - const err = get(errs, '0.0.message') - if (err != null) { - $message.error(err) - } else { - $message.error(i18n.t('dialogue.spec_field_required', { key: i18n.t('dialogue.field.element') })) - } - }) + if (subFormRef.value?.validate) { + await subFormRef.value?.validate((errs) => { + const err = get(errs, '0.0.message') + if (err != null) { + $message.error(err) + } else { + $message.error(i18n.t('dialogue.spec_field_required', { key: i18n.t('dialogue.field.element') })) + } + }) + } try { const { server, db, key, type, ttl } = newForm let { value } = newForm diff --git a/frontend/src/components/icons/LoadAll.vue b/frontend/src/components/icons/LoadAll.vue index 81dd160..2448cd1 100644 --- a/frontend/src/components/icons/LoadAll.vue +++ b/frontend/src/components/icons/LoadAll.vue @@ -12,13 +12,13 @@ const props = defineProps({ - + diff --git a/frontend/src/components/icons/More.vue b/frontend/src/components/icons/More.vue index 139aa18..6692bf8 100644 --- a/frontend/src/components/icons/More.vue +++ b/frontend/src/components/icons/More.vue @@ -9,15 +9,9 @@ const props = defineProps({ - - - - + + + diff --git a/frontend/src/components/icons/Plus.vue b/frontend/src/components/icons/Plus.vue new file mode 100644 index 0000000..58d0670 --- /dev/null +++ b/frontend/src/components/icons/Plus.vue @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/frontend/src/components/icons/Search.vue b/frontend/src/components/icons/Search.vue index a2c2161..0b7cf67 100644 --- a/frontend/src/components/icons/Search.vue +++ b/frontend/src/components/icons/Search.vue @@ -17,13 +17,7 @@ const props = defineProps({ stroke-linejoin="round" /> - diff --git a/frontend/src/components/sidebar/BrowserPane.vue b/frontend/src/components/sidebar/BrowserPane.vue index 951ca63..9f6ae5e 100644 --- a/frontend/src/components/sidebar/BrowserPane.vue +++ b/frontend/src/components/sidebar/BrowserPane.vue @@ -3,54 +3,171 @@ 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, reactive, ref } from 'vue' -import { get } from 'lodash' +import { computed, onMounted, reactive, ref, unref, watch } from 'vue' +import { get, map } from 'lodash' import Refresh from '@/components/icons/Refresh.vue' import useDialogStore from 'stores/dialog.js' import { useI18n } from 'vue-i18n' -import { types } from '@/consts/support_redis_type.js' import Search from '@/components/icons/Search.vue' import Unlink from '@/components/icons/Unlink.vue' -import Filter from '@/components/icons/Filter.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' const themeVars = useThemeVars() +const i18n = useI18n() const dialogStore = useDialogStore() +// const prefStore = usePreferencesStore() const tabStore = useTabStore() +const browserStore = useBrowserStore() +const render = useRender() const currentName = computed(() => get(tabStore.currentTab, 'name', '')) const browserTreeRef = ref(null) +const loading = ref(false) +const fullyLoaded = ref(false) -const onInfo = () => { - browserTreeRef.value?.handleSelectContextMenu('server_info') +const selectedDB = computed(() => { + return browserStore.selectedDatabases[currentName.value] || 0 +}) + +const dbSelectOptions = computed(() => { + const dblist = browserStore.getDBList(currentName.value) + return map(dblist, (db) => { + if (selectedDB.value === db.db) { + return { + value: db.db, + label: `db${db.db} (${db.keys}/${db.maxKeys})`, + } + } + return { + value: db.db, + label: `db${db.db} (${db.maxKeys})`, + } + }) +}) + +const loadProgress = computed(() => { + const db = browserStore.getDatabase(currentName.value, selectedDB.value) + if (db.maxKeys <= 0) { + return 100 + } + return (db.keys * 100) / db.maxKeys +}) + +const onReload = async () => { + try { + loading.value = true + tabStore.setSelectedKeys(currentName.value) + const db = selectedDB.value + browserStore.closeDatabase(currentName.value, db) + browserTreeRef.value?.resetExpandKey(currentName.value, db) + + let matchType = unref(filterForm.type) + if (!types.hasOwnProperty(matchType)) { + matchType = '' + } + browserStore.setKeyFilter(currentName.value, { + type: matchType, + pattern: unref(filterForm.pattern), + }) + await browserStore.openDatabase(currentName.value, db) + fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, db) + // $message.success(i18n.t('dialogue.reload_succ')) + } catch (e) { + console.warn(e) + } finally { + loading.value = false + } +} + +const onAddKey = () => { + dialogStore.openNewKeyDialog('', currentName.value, selectedDB.value) +} + +const onLoadMore = async () => { + try { + loading.value = true + fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, selectedDB.value) + } catch (e) { + $message.error(e.message) + } finally { + loading.value = false + } +} + +const onLoadAll = async () => { + try { + loading.value = true + await browserStore.loadAllKeys(currentName.value, selectedDB.value) + fullyLoaded.value = true + } catch (e) { + $message.error(e.message) + } finally { + loading.value = false + } +} + +const onFlush = () => { + dialogStore.openFlushDBDialog(currentName.value, selectedDB.value) } -const i18n = useI18n() const onDisconnect = () => { - browserTreeRef.value?.handleSelectContextMenu('server_close') + browserStore.closeConnection(currentName.value) } -const onRefresh = () => { - browserTreeRef.value?.handleSelectContextMenu('server_reload') +const handleSelectDB = async (db, prevDB) => { + // watch 'browserStore.openedDB[currentName.value]' instead } const filterForm = reactive({ - showFilter: false, type: '', pattern: '', + filter: '', }) +const onSelectFilterType = (select) => { + onReload() +} -const filterTypeOptions = computed(() => { - const options = Object.keys(types).map((t) => ({ - value: t, - label: t, - })) - options.splice(0, 0, { - value: '', - label: i18n.t('common.all'), - }) - return options -}) +const onFilterInput = (val) => { + filterForm.filter = val +} +const onMatchInput = (matchVal, filterVal) => { + filterForm.pattern = matchVal + filterForm.filter = filterVal + onReload() +} + +watch( + () => browserStore.openedDB[currentName.value], + async (db, prevDB) => { + if (db === undefined) { + return + } + + try { + loading.value = true + browserStore.closeDatabase(currentName.value, prevDB) + browserStore.setKeyFilter(currentName.value, {}) + await browserStore.openDatabase(currentName.value, db) + browserTreeRef.value?.resetExpandKey(currentName.value, db) + fullyLoaded.value = await browserStore.loadMoreKeys(currentName.value, db) + browserTreeRef.value?.refreshTree() + } catch (e) { + $message.error(e.message) + } finally { + loading.value = false + } + }, +) + +onMounted(() => onReload()) // forbid dynamic switch key view due to performance issues // const viewType = ref(0) // const onSwitchView = (selectView) => { @@ -61,73 +178,110 @@ const filterTypeOptions = computed(() => { - - s + + + + + + + + + + + + + + + + + {{ $t('interface.reload') }} + + + + + + + + + + {{ $t('interface.new_key') }} + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + t-tooltip="interface.load_more" + @click="onLoadMore" /> + + @@ -136,21 +290,30 @@ const filterTypeOptions = computed(() => {