Compare commits

..

No commits in common. "718e89a641082744b746a157666c83f678910ab8" and "cd36dbda48d5f4cd2c4f4fc03809dec009ce778a" have entirely different histories.

12 changed files with 135 additions and 239 deletions

View File

@ -291,8 +291,7 @@ func (b *browserService) getRedisClient(server string, db int) (item *connection
var ok bool var ok bool
var client redis.UniversalClient var client redis.UniversalClient
if item, ok = b.connMap[server]; ok { if item, ok = b.connMap[server]; ok {
if item.db == db || db < 0 { if item.db == db {
// return without switch database directly
return return
} }

View File

@ -1,69 +0,0 @@
<script setup>
import { isNumber } from 'lodash'
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
on: {
type: Boolean,
default: false,
},
defaultValue: {
type: Number,
default: 2,
},
interval: {
type: Number,
default: 2,
},
onRefresh: {
type: Function,
default: () => {},
},
})
const emit = defineEmits(['toggle', 'update:on', 'update:interval'])
const onToggle = (on) => {
emit('update:on', on === true)
if (on) {
let interval = props.interval
if (!isNumber(interval)) {
interval = props.defaultValue
}
interval = Math.max(1, interval)
emit('update:interval', interval)
emit('toggle', true)
} else {
emit('toggle', false)
}
}
</script>
<template>
<n-form :show-feedback="false" label-align="right" label-placement="left" label-width="auto" size="small">
<n-form-item :label="$t('interface.auto_refresh')">
<n-switch :loading="props.loading" :value="props.on" @update:value="onToggle" />
</n-form-item>
<n-form-item :label="$t('interface.refresh_interval')">
<n-input-number
:autofocus="false"
:default-value="props.defaultValue"
:disabled="props.on"
:max="9999"
:min="1"
:show-button="false"
:value="props.interval"
style="max-width: 100px"
@update:value="(val) => emit('update:interval', val)">
<template #suffix>
{{ $t('common.unit_second') }}
</template>
</n-input-number>
</n-form-item>
</n-form>
</template>
<style lang="scss" scoped></style>

View File

@ -107,8 +107,7 @@ const loadHistory = async () => {
data.history = list || [] data.history = list || []
} finally { } finally {
data.loading = false data.loading = false
await nextTick() tableRef.value?.scrollTo({ top: 999999 })
tableRef.value?.scrollTo({ position: 'bottom' })
} }
} }
@ -119,8 +118,7 @@ const cleanHistory = async () => {
const success = await browserStore.cleanCmdHistory() const success = await browserStore.cleanCmdHistory()
if (success) { if (success) {
data.history = [] data.history = []
await nextTick() tableRef.value?.scrollTo({ top: 0 })
tableRef.value?.scrollTo({ position: 'top' })
$message.success(i18n.t('dialogue.handle_succ')) $message.success(i18n.t('dialogue.handle_succ'))
} }
} finally { } finally {

View File

@ -1,27 +1,20 @@
<script setup> <script setup>
import { get, isEmpty, map, mapValues, pickBy, split, sum, toArray, toNumber } from 'lodash' import { get, isEmpty, map, mapValues, pickBy, split, sum, toArray, toNumber } from 'lodash'
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import Filter from '@/components/icons/Filter.vue' import Filter from '@/components/icons/Filter.vue'
import Refresh from '@/components/icons/Refresh.vue' import Refresh from '@/components/icons/Refresh.vue'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { timeout } from '@/utils/promise.js'
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
import { NIcon, useThemeVars } from 'naive-ui'
const props = defineProps({ const props = defineProps({
server: String, server: String,
}) })
const browserStore = useBrowserStore() const browserStore = useBrowserStore()
const themeVars = useThemeVars()
const serverInfo = ref({}) const serverInfo = ref({})
const pageState = reactive({ const autoRefresh = ref(false)
autoRefresh: false, const loading = ref(false) // loading status for refresh
refreshInterval: 5, const autoLoading = ref(false) // loading status for auto refresh
loading: false, // loading status for refresh
autoLoading: false, // loading status for auto refresh
})
/** /**
* refresh server status info * refresh server status info
@ -30,58 +23,32 @@ const pageState = reactive({
*/ */
const refreshInfo = async (force) => { const refreshInfo = async (force) => {
if (force) { if (force) {
pageState.loading = true loading.value = true
} else { } else {
pageState.autoLoading = true autoLoading.value = true
} }
if (!isEmpty(props.server) && browserStore.isConnected(props.server)) { if (!isEmpty(props.server) && browserStore.isConnected(props.server)) {
try { try {
serverInfo.value = await browserStore.getServerInfo(props.server) serverInfo.value = await browserStore.getServerInfo(props.server)
} finally { } finally {
pageState.loading = false loading.value = false
pageState.autoLoading = false autoLoading.value = false
} }
} }
} }
const isLoading = computed(() => { let intervalID
return pageState.loading || pageState.autoLoading
})
const startAutoRefresh = async () => {
let lastExec = Date.now()
do {
if (!pageState.autoRefresh) {
break
}
await timeout(100)
if (pageState.loading || pageState.autoLoading || Date.now() - lastExec < pageState.refreshInterval * 1000) {
continue
}
lastExec = Date.now()
await refreshInfo()
} while (true)
stopAutoRefresh()
}
const stopAutoRefresh = () => {
pageState.autoRefresh = false
}
const onToggleRefresh = (on) => {
if (on) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
onMounted(() => { onMounted(() => {
refreshInfo() refreshInfo()
intervalID = setInterval(() => {
if (autoRefresh.value === true) {
refreshInfo()
}
}, 5000)
}) })
onUnmounted(() => { onUnmounted(() => {
stopAutoRefresh() clearInterval(intervalID)
}) })
const scrollRef = ref(null) const scrollRef = ref(null)
@ -161,7 +128,7 @@ const infoFilter = ref('')
<n-tag size="small" type="primary">{{ redisMode }}</n-tag> <n-tag size="small" type="primary">{{ redisMode }}</n-tag>
</template> </template>
</n-tooltip> </n-tooltip>
<n-tooltip v-if="role"> <n-tooltip v-if="redisMode">
Role Role
<template #trigger> <template #trigger>
<n-tag size="small" type="primary">{{ role }}</n-tag> <n-tag size="small" type="primary">{{ role }}</n-tag>
@ -170,36 +137,27 @@ const infoFilter = ref('')
</n-space> </n-space>
</template> </template>
<template #header-extra> <template #header-extra>
<n-popover keep-alive-on-hover placement="bottom-end" trigger="hover"> <n-space align="center" inline>
{{ $t('status.auto_refresh') }}
<n-switch v-model:value="autoRefresh" :loading="autoLoading" />
<n-tooltip>
{{ $t('status.refresh') }}
<template #trigger> <template #trigger>
<n-button <n-button
:loading="pageState.loading" :loading="autoLoading"
:type="isLoading ? 'primary' : 'default'"
circle circle
size="small" size="small"
tertiary tertiary
@click="refreshInfo(true)"> @click="refreshInfo(true)">
<template #icon> <template #icon>
<n-icon :size="props.size"> <n-icon :component="Refresh" />
<refresh
:class="{
'auto-rotate': pageState.autoRefresh || isLoading,
}"
:color="pageState.autoRefresh ? themeVars.primaryColor : undefined"
:stroke-width="pageState.autoRefresh ? 6 : 3" />
</n-icon>
</template> </template>
</n-button> </n-button>
</template> </template>
<auto-refresh-form </n-tooltip>
v-model:interval="pageState.refreshInterval" </n-space>
v-model:on="pageState.autoRefresh"
:default-value="5"
:loading="pageState.autoLoading"
@toggle="onToggleRefresh" />
</n-popover>
</template> </template>
<n-spin :show="pageState.loading"> <n-spin :show="loading">
<n-grid style="min-width: 500px" x-gap="5"> <n-grid style="min-width: 500px" x-gap="5">
<n-gi :span="6"> <n-gi :span="6">
<n-statistic :label="$t('status.uptime')" :value="uptime[0]"> <n-statistic :label="$t('status.uptime')" :value="uptime[0]">
@ -234,7 +192,7 @@ const infoFilter = ref('')
</template> </template>
</n-input> </n-input>
</template> </template>
<n-spin :show="pageState.loading"> <n-spin :show="loading">
<n-tabs default-value="CPU" placement="left" type="line"> <n-tabs default-value="CPU" placement="left" type="line">
<n-tab-pane v-for="(v, k) in serverInfo" :key="k" :disabled="isEmpty(v)" :name="k"> <n-tab-pane v-for="(v, k) in serverInfo" :key="k" :disabled="isEmpty(v)" :name="k">
<n-data-table <n-data-table

View File

@ -1,13 +1,11 @@
<script setup> <script setup>
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue' import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue'
import Refresh from '@/components/icons/Refresh.vue' import Refresh from '@/components/icons/Refresh.vue'
import { debounce, isEmpty, map, size, split } from 'lodash' import { debounce, isEmpty, map, size, split } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import useBrowserStore from 'stores/browser.js' import useBrowserStore from 'stores/browser.js'
import { timeout } from '@/utils/promise.js'
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -23,16 +21,12 @@ const props = defineProps({
}, },
}) })
const autoRefresh = reactive({
on: false,
interval: 5,
})
const data = reactive({ const data = reactive({
list: [], list: [],
sortOrder: 'descend', sortOrder: 'descend',
listLimit: 20, listLimit: 20,
loading: false, loading: false,
autoLoading: false,
client: '', client: '',
keyword: '', keyword: '',
}) })
@ -131,45 +125,26 @@ const _loadSlowLog = () => {
.then((list) => { .then((list) => {
data.list = list || [] data.list = list || []
}) })
.finally(async () => { .finally(() => {
data.loading = false data.loading = false
await nextTick() tableRef.value?.scrollTo({ top: data.sortOrder === 'ascend' ? 999999 : 0 })
tableRef.value?.scrollTo({ position: data.sortOrder === 'ascend' ? 'bottom' : 'top' })
}) })
} }
const loadSlowLog = debounce(_loadSlowLog, 1000, { leading: true, trailing: true }) const loadSlowLog = debounce(_loadSlowLog, 1000, { leading: true, trailing: true })
const startAutoRefresh = async () => { let intervalID
let lastExec = Date.now() onMounted(() => {
do { loadSlowLog()
if (!autoRefresh.on) { intervalID = setInterval(() => {
break if (data.autoLoading === true) {
}
await timeout(100)
if (data.loading || Date.now() - lastExec < autoRefresh.interval * 1000) {
continue
}
lastExec = Date.now()
loadSlowLog() loadSlowLog()
} while (true)
stopAutoRefresh()
}
const stopAutoRefresh = () => {
autoRefresh.on = false
}
onMounted(() => loadSlowLog())
onUnmounted(() => stopAutoRefresh())
const onToggleRefresh = (on) => {
if (on) {
startAutoRefresh()
} else {
stopAutoRefresh()
} }
} }, 5000)
})
onUnmounted(() => {
clearInterval(intervalID)
})
const onListLimitChanged = (limit) => { const onListLimitChanged = (limit) => {
loadSlowLog() loadSlowLog()
@ -186,28 +161,23 @@ const onListLimitChanged = (limit) => {
style="width: 120px" style="width: 120px"
@update:value="onListLimitChanged" /> @update:value="onListLimitChanged" />
</n-form-item> </n-form-item>
<n-form-item :label="$t('slog.filter')"> <n-form-item :label="$t('slog.auto_refresh')">
<n-input v-model:value="data.keyword" clearable placeholder="" /> <n-switch v-model:value="data.autoLoading" :loading="data.loading" />
</n-form-item> </n-form-item>
<n-form-item label="&nbsp;"> <n-form-item label="&nbsp;">
<n-popover :delay="500" keep-alive-on-hover placement="bottom" trigger="hover"> <n-tooltip>
{{ $t('slog.refresh') }}
<template #trigger> <template #trigger>
<n-button :loading="data.loading" circle size="small" tertiary @click="_loadSlowLog"> <n-button :loading="data.loading" circle size="small" tertiary @click="_loadSlowLog">
<template #icon> <template #icon>
<refresh <n-icon :component="Refresh" />
:class="{ 'auto-rotate': autoRefresh.on }"
:color="autoRefresh.on ? themeVars.primaryColor : undefined"
:stroke-width="autoRefresh.on ? 6 : 3" />
</template> </template>
</n-button> </n-button>
</template> </template>
<auto-refresh-form </n-tooltip>
v-model:interval="autoRefresh.interval" </n-form-item>
v-model:on="autoRefresh.on" <n-form-item :label="$t('slog.filter')">
:default-value="5" <n-input v-model:value="data.keyword" clearable placeholder="" />
:loading="data.loading"
@toggle="onToggleRefresh" />
</n-popover>
</n-form-item> </n-form-item>
</n-form> </n-form>
<n-data-table <n-data-table

View File

@ -14,7 +14,6 @@ import { computed, onUnmounted, reactive, watch } from 'vue'
import { padStart } from 'lodash' import { padStart } from 'lodash'
import { NIcon, useThemeVars } from 'naive-ui' import { NIcon, useThemeVars } from 'naive-ui'
import { timeout } from '@/utils/promise.js' import { timeout } from '@/utils/promise.js'
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -71,6 +70,14 @@ const ttlString = computed(() => {
}) })
const startAutoRefresh = async () => { const startAutoRefresh = async () => {
if (autoRefresh.on) {
return
}
autoRefresh.on = true
if (!isNaN(autoRefresh.interval)) {
autoRefresh.interval = 2
}
autoRefresh.interval = Math.min(autoRefresh.interval, 1)
let lastExec = Date.now() let lastExec = Date.now()
do { do {
if (!autoRefresh.on) { if (!autoRefresh.on) {
@ -140,19 +147,42 @@ const onTTL = () => {
<template #trigger> <template #trigger>
<icon-button :loading="props.loading" size="18" @click="emit('reload')"> <icon-button :loading="props.loading" size="18" @click="emit('reload')">
<n-icon :size="props.size"> <n-icon :size="props.size">
<refresh <component
:class="{ 'auto-rotate': autoRefresh.on }" :is="Refresh"
:class="{ 'auto-refreshing': autoRefresh.on }"
:color="autoRefresh.on ? themeVars.primaryColor : undefined" :color="autoRefresh.on ? themeVars.primaryColor : undefined"
:stroke-width="autoRefresh.on ? 6 : 3" /> :stroke-width="autoRefresh.on ? 5 : 3" />
</n-icon> </n-icon>
</icon-button> </icon-button>
</template> </template>
<auto-refresh-form <n-form
v-model:interval="autoRefresh.interval" :show-feedback="false"
v-model:on="autoRefresh.on" label-align="right"
:default-value="2" label-placement="left"
label-width="auto"
size="small">
<n-form-item :label="$t('interface.auto_refresh')">
<n-switch
:loading="props.loading" :loading="props.loading"
@toggle="onToggleRefresh" /> :value="autoRefresh.on"
@update:value="onToggleRefresh" />
</n-form-item>
<n-form-item :label="$t('interface.refresh_interval')">
<n-input-number
v-model:value="autoRefresh.interval"
:autofocus="false"
:default-value="2"
:disabled="autoRefresh.on"
:max="9999"
:min="1"
:show-button="false"
style="max-width: 100px">
<template #suffix>
{{ $t('common.unit_second') }}
</template>
</n-input-number>
</n-form-item>
</n-form>
</n-popover> </n-popover>
</template> </template>
</n-input> </n-input>
@ -196,4 +226,14 @@ const onTTL = () => {
align-items: center; align-items: center;
gap: 5px; gap: 5px;
} }
.auto-refreshing {
animation: rotate 2s linear infinite;
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
</style> </style>

View File

@ -16,14 +16,14 @@ const props = defineProps({
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
d="M36.7279 36.7279C33.4706 39.9853 28.9706 42 24 42C14.0589 42 6 33.9411 6 24C6 14.0589 14.0589 6 24 6C28.9706 6 33.4706 8.01472 36.7279 11.2721C38.3859 12.9301 42 17 42 17" d="M36.7279 36.7279C33.4706 39.9853 28.9706 42 24 42C14.0589 42 6 33.9411 6 24C6 14.0589 14.0589 6 24 6C28.9706 6 33.4706 8.01472 36.7279 11.2721C38.3859 12.9301 42 17 42 17"
:stroke="color" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
<path <path
:stroke-width="props.strokeWidth" :stroke-width="props.strokeWidth"
class="default-stroke" class="default-stroke"
d="M42 8V17H33" d="M42 8V17H33"
:stroke="color" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" /> stroke-linejoin="round" />
</svg> </svg>

View File

@ -359,7 +359,9 @@
"connected_clients": "Clients", "connected_clients": "Clients",
"total_keys": "Keys", "total_keys": "Keys",
"memory_used": "Memory", "memory_used": "Memory",
"all_info": "Information" "all_info": "Information",
"refresh": "Refresh",
"auto_refresh": "Auto Refresh"
}, },
"slog": { "slog": {
"title": "Slow Log", "title": "Slow Log",
@ -368,7 +370,9 @@
"exec_time": "Time", "exec_time": "Time",
"client": "Client", "client": "Client",
"cmd": "Command", "cmd": "Command",
"cost_time": "Cost" "cost_time": "Cost",
"refresh": "Refresh Now",
"auto_refresh": "Auto Refresh"
}, },
"monitor": { "monitor": {
"title": "Monitor Commands", "title": "Monitor Commands",

View File

@ -291,7 +291,9 @@
"connected_clients": "Clientes Conectados", "connected_clients": "Clientes Conectados",
"total_keys": "Chaves Totais", "total_keys": "Chaves Totais",
"memory_used": "Memória Usada", "memory_used": "Memória Usada",
"all_info": "Informações" "all_info": "Informações",
"refresh": "Atualizar",
"auto_refresh": "Atualização Automática"
}, },
"slog": { "slog": {
"title": "Registro de Operações Lentas", "title": "Registro de Operações Lentas",
@ -300,6 +302,8 @@
"exec_time": "Tempo", "exec_time": "Tempo",
"client": "Cliente", "client": "Cliente",
"cmd": "Comando", "cmd": "Comando",
"cost_time": "Custo" "cost_time": "Custo",
"refresh": "Atualizar Agora",
"auto_refresh": "Atualização Automática"
} }
} }

View File

@ -359,7 +359,9 @@
"connected_clients": "已连客户端", "connected_clients": "已连客户端",
"total_keys": "键总数", "total_keys": "键总数",
"memory_used": "内存使用", "memory_used": "内存使用",
"all_info": "全部信息" "all_info": "全部信息",
"refresh": "立即刷新",
"auto_refresh": "自动刷新"
}, },
"slog": { "slog": {
"title": "慢日志", "title": "慢日志",
@ -368,7 +370,9 @@
"exec_time": "执行时间", "exec_time": "执行时间",
"client": "客户端", "client": "客户端",
"cmd": "命令", "cmd": "命令",
"cost_time": "耗时" "cost_time": "耗时",
"refresh": "立即刷新",
"auto_refresh": "自动刷新"
}, },
"monitor": { "monitor": {
"title": "监控命令", "title": "监控命令",

View File

@ -347,7 +347,7 @@ const useBrowserStore = defineStore('browser', {
*/ */
async getServerInfo(server) { async getServerInfo(server) {
try { try {
const { success, data, msg } = await ServerInfo(server) const { success, data } = await ServerInfo(server)
if (success) { if (success) {
/** @type {RedisServerState} **/ /** @type {RedisServerState} **/
const serverInst = this.servers[server] const serverInst = this.servers[server]
@ -355,8 +355,6 @@ const useBrowserStore = defineStore('browser', {
serverInst.stats = data serverInst.stats = data
} }
return data return data
} else if (!isEmpty(msg)) {
$message.warning(msg)
} }
} finally { } finally {
} }

View File

@ -168,13 +168,3 @@ body {
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }
.auto-rotate {
animation: rotate 2s linear infinite;
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}