feat: add activity monitor
This commit is contained in:
parent
70235cc295
commit
ee57346df6
|
@ -1,6 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { get, isEmpty, map, mapValues, pickBy, split, sum, toArray, toNumber } from 'lodash'
|
import { cloneDeep, flatMap, get, isEmpty, map, mapValues, pickBy, slice, split, sum, toArray, toNumber } from 'lodash'
|
||||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, toRaw } 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'
|
||||||
|
@ -8,6 +8,10 @@ import useBrowserStore from 'stores/browser.js'
|
||||||
import { timeout } from '@/utils/promise.js'
|
import { timeout } from '@/utils/promise.js'
|
||||||
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
import AutoRefreshForm from '@/components/common/AutoRefreshForm.vue'
|
||||||
import { NIcon, useThemeVars } from 'naive-ui'
|
import { NIcon, useThemeVars } from 'naive-ui'
|
||||||
|
import { Line } from 'vue-chartjs'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { convertBytes, formatBytes } from '@/utils/byte_convert.js'
|
||||||
|
import { i18n } from '@/utils/i18n.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
server: String,
|
server: String,
|
||||||
|
@ -22,6 +26,24 @@ const pageState = reactive({
|
||||||
loading: false, // loading status for refresh
|
loading: false, // loading status for refresh
|
||||||
autoLoading: false, // loading status for auto refresh
|
autoLoading: false, // loading status for auto refresh
|
||||||
})
|
})
|
||||||
|
const statusHistory = 5
|
||||||
|
const cmdRateRef = ref(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param origin
|
||||||
|
* @param {string[]} labels
|
||||||
|
* @param {number[][]} datalist
|
||||||
|
* @return {unknown}
|
||||||
|
*/
|
||||||
|
const generateData = (origin, labels, datalist) => {
|
||||||
|
let ret = toRaw(origin)
|
||||||
|
ret.labels = labels
|
||||||
|
for (let i = 0; i < datalist.length; i++) {
|
||||||
|
ret.datasets[i].data = datalist[i]
|
||||||
|
}
|
||||||
|
return cloneDeep(ret)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* refresh server status info
|
* refresh server status info
|
||||||
|
@ -36,7 +58,9 @@ const refreshInfo = async (force) => {
|
||||||
}
|
}
|
||||||
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)
|
const info = await browserStore.getServerInfo(props.server)
|
||||||
|
serverInfo.value = info
|
||||||
|
_updateChart(info)
|
||||||
} finally {
|
} finally {
|
||||||
pageState.loading = false
|
pageState.loading = false
|
||||||
pageState.autoLoading = false
|
pageState.autoLoading = false
|
||||||
|
@ -44,6 +68,54 @@ const refreshInfo = async (force) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _updateChart = (info) => {
|
||||||
|
let timeLabels = toRaw(cmdRate.value.labels)
|
||||||
|
timeLabels = timeLabels.concat(dayjs().format('hh:mm:ss'))
|
||||||
|
timeLabels = slice(timeLabels, Math.max(0, timeLabels.length - statusHistory))
|
||||||
|
|
||||||
|
// commands per seconds
|
||||||
|
{
|
||||||
|
let dataset = toRaw(cmdRate.value.datasets[0].data)
|
||||||
|
const cmd = parseInt(get(info, 'Stats.instantaneous_ops_per_sec', '0'))
|
||||||
|
dataset = dataset.concat(cmd)
|
||||||
|
dataset = slice(dataset, Math.max(0, dataset.length - statusHistory))
|
||||||
|
cmdRate.value = generateData(cmdRate.value, timeLabels, [dataset])
|
||||||
|
}
|
||||||
|
|
||||||
|
// connected clients
|
||||||
|
{
|
||||||
|
let dataset = toRaw(connectedClients.value.datasets[0].data)
|
||||||
|
const count = parseInt(get(info, 'Clients.connected_clients', '0'))
|
||||||
|
dataset = dataset.concat(count)
|
||||||
|
dataset = slice(dataset, Math.max(0, dataset.length - statusHistory))
|
||||||
|
connectedClients.value = generateData(connectedClients.value, timeLabels, [dataset])
|
||||||
|
}
|
||||||
|
|
||||||
|
// memory usage
|
||||||
|
{
|
||||||
|
let dataset = toRaw(memoryUsage.value.datasets[0].data)
|
||||||
|
let size = parseInt(get(info, 'Memory.used_memory', '0'))
|
||||||
|
dataset = dataset.concat(size)
|
||||||
|
dataset = slice(dataset, Math.max(0, dataset.length - statusHistory))
|
||||||
|
memoryUsage.value = generateData(memoryUsage.value, timeLabels, [dataset])
|
||||||
|
}
|
||||||
|
|
||||||
|
// network input/output rate
|
||||||
|
{
|
||||||
|
let dataset1 = toRaw(networkRate.value.datasets[0].data)
|
||||||
|
const input = parseInt(get(info, 'Stats.instantaneous_input_kbps', '0'))
|
||||||
|
dataset1 = dataset1.concat(input)
|
||||||
|
dataset1 = slice(dataset1, Math.max(0, dataset1.length - statusHistory))
|
||||||
|
|
||||||
|
let dataset2 = toRaw(networkRate.value.datasets[1].data)
|
||||||
|
const output = parseInt(get(info, 'Stats.instantaneous_output_kbps', '0'))
|
||||||
|
dataset2 = dataset2.concat(output)
|
||||||
|
dataset2 = slice(dataset2, Math.max(0, dataset2.length - statusHistory))
|
||||||
|
networkRate.value = generateData(networkRate.value, timeLabels, [dataset1, dataset2])
|
||||||
|
// console.log(dataset1, dataset2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isLoading = computed(() => {
|
const isLoading = computed(() => {
|
||||||
return pageState.loading || pageState.autoLoading
|
return pageState.loading || pageState.autoLoading
|
||||||
})
|
})
|
||||||
|
@ -70,6 +142,7 @@ const stopAutoRefresh = () => {
|
||||||
|
|
||||||
const onToggleRefresh = (on) => {
|
const onToggleRefresh = (on) => {
|
||||||
if (on) {
|
if (on) {
|
||||||
|
tabVal.value = 'activity'
|
||||||
startAutoRefresh()
|
startAutoRefresh()
|
||||||
} else {
|
} else {
|
||||||
stopAutoRefresh()
|
stopAutoRefresh()
|
||||||
|
@ -84,7 +157,6 @@ onUnmounted(() => {
|
||||||
stopAutoRefresh()
|
stopAutoRefresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
const scrollRef = ref(null)
|
|
||||||
const redisVersion = computed(() => {
|
const redisVersion = computed(() => {
|
||||||
return get(serverInfo.value, 'Server.redis_version', '')
|
return get(serverInfo.value, 'Server.redis_version', '')
|
||||||
})
|
})
|
||||||
|
@ -99,31 +171,24 @@ const role = computed(() => {
|
||||||
|
|
||||||
const timeUnit = ['common.unit_minute', 'common.unit_hour', 'common.unit_day']
|
const timeUnit = ['common.unit_minute', 'common.unit_hour', 'common.unit_day']
|
||||||
const uptime = computed(() => {
|
const uptime = computed(() => {
|
||||||
let seconds = get(serverInfo.value, 'Server.uptime_in_seconds', 0)
|
let seconds = parseInt(get(serverInfo.value, 'Server.uptime_in_seconds', '0'))
|
||||||
seconds /= 60
|
seconds /= 60
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
// minutes
|
// minutes
|
||||||
return [Math.floor(seconds), timeUnit[0]]
|
return { value: Math.floor(seconds), unit: timeUnit[0] }
|
||||||
}
|
}
|
||||||
seconds /= 60
|
seconds /= 60
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
// hours
|
// hours
|
||||||
return [Math.floor(seconds), timeUnit[1]]
|
return { value: Math.floor(seconds), unit: timeUnit[1] }
|
||||||
}
|
}
|
||||||
return [Math.floor(seconds / 24), timeUnit[2]]
|
return { value: Math.floor(seconds / 24), unit: timeUnit[2] }
|
||||||
})
|
})
|
||||||
|
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
||||||
const usedMemory = computed(() => {
|
const usedMemory = computed(() => {
|
||||||
let size = get(serverInfo.value, 'Memory.used_memory', 0)
|
let size = parseInt(get(serverInfo.value, 'Memory.used_memory', '0'))
|
||||||
let unitIndex = 0
|
const { value, unit } = convertBytes(size)
|
||||||
|
return [value, unit]
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
||||||
size /= 1024
|
|
||||||
unitIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
return [size.toFixed(2), units[unitIndex]]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalKeys = computed(() => {
|
const totalKeys = computed(() => {
|
||||||
|
@ -138,127 +203,341 @@ const totalKeys = computed(() => {
|
||||||
})
|
})
|
||||||
return sum(toArray(nums))
|
return sum(toArray(nums))
|
||||||
})
|
})
|
||||||
const infoFilter = ref('')
|
|
||||||
|
const tabVal = ref('info')
|
||||||
|
const envFilter = reactive({
|
||||||
|
keyword: '',
|
||||||
|
group: 'CPU',
|
||||||
|
})
|
||||||
|
|
||||||
|
const env = computed(() => {
|
||||||
|
if (!isEmpty(envFilter.group)) {
|
||||||
|
const val = serverInfo.value[envFilter.group]
|
||||||
|
if (!isEmpty(val)) {
|
||||||
|
return map(val, (v, k) => ({
|
||||||
|
key: k,
|
||||||
|
value: v,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flatMap(serverInfo.value, (value, key) => {
|
||||||
|
return map(value, (v, k) => ({
|
||||||
|
group: key,
|
||||||
|
key: k,
|
||||||
|
value: v,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const onFilterGroup = (group) => {
|
||||||
|
if (group === envFilter.group) {
|
||||||
|
envFilter.group = ''
|
||||||
|
} else {
|
||||||
|
envFilter.group = group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartBGColor = [
|
||||||
|
'rgba(255, 99, 132, 0.2)',
|
||||||
|
'rgba(255, 159, 64, 0.2)',
|
||||||
|
'rgba(153, 102, 255, 0.2)',
|
||||||
|
'rgba(75, 192, 192, 0.2)',
|
||||||
|
'rgba(54, 162, 235, 0.2)',
|
||||||
|
]
|
||||||
|
|
||||||
|
const chartBorderColor = [
|
||||||
|
'rgb(255, 99, 132)',
|
||||||
|
'rgb(255, 159, 64)',
|
||||||
|
'rgb(153, 102, 255)',
|
||||||
|
'rgb(75, 192, 192)',
|
||||||
|
'rgb(54, 162, 235)',
|
||||||
|
]
|
||||||
|
|
||||||
|
const cmdRate = ref({
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('status.act_cmd'),
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: chartBGColor[0],
|
||||||
|
borderColor: chartBorderColor[0],
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectedClients = ref({
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('status.connected_clients'),
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: chartBGColor[1],
|
||||||
|
borderColor: chartBorderColor[1],
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const memoryUsage = ref({
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('status.memory_used'),
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: chartBGColor[2],
|
||||||
|
borderColor: chartBorderColor[2],
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const networkRate = ref({
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('status.act_network_input'),
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: chartBGColor[3],
|
||||||
|
borderColor: chartBorderColor[3],
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.global.t('status.act_network_output'),
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: chartBGColor[4],
|
||||||
|
borderColor: chartBorderColor[4],
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartOption = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
events: [],
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
suggestedMin: 0,
|
||||||
|
ticks: {
|
||||||
|
precision: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteChartOption = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
events: [],
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
suggestedMin: 0,
|
||||||
|
ticks: {
|
||||||
|
precision: 0,
|
||||||
|
// format display y axios tag
|
||||||
|
callback: function (value, index, values) {
|
||||||
|
return formatBytes(value, 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-scrollbar ref="scrollRef">
|
<n-space :size="5" :wrap-item="false" style="padding: 5px; box-sizing: border-box; height: 100%" vertical>
|
||||||
<n-back-top :listen-to="scrollRef" />
|
<n-card embedded>
|
||||||
<n-space :size="5" :wrap-item="false" style="padding: 5px" vertical>
|
<template #header>
|
||||||
<n-card embedded>
|
<n-space :wrap-item="false" align="center" inline size="small">
|
||||||
<template #header>
|
{{ props.server }}
|
||||||
<n-space :wrap-item="false" align="center" inline size="small">
|
<n-tooltip v-if="redisVersion">
|
||||||
{{ props.server }}
|
Redis Version
|
||||||
<n-tooltip v-if="redisVersion">
|
|
||||||
Redis Version
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag size="small" type="primary">v{{ redisVersion }}</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-tooltip>
|
|
||||||
<n-tooltip v-if="redisMode">
|
|
||||||
Mode
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag size="small" type="primary">{{ redisMode }}</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-tooltip>
|
|
||||||
<n-tooltip v-if="role">
|
|
||||||
Role
|
|
||||||
<template #trigger>
|
|
||||||
<n-tag size="small" type="primary">{{ role }}</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-tooltip>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
<template #header-extra>
|
|
||||||
<n-popover keep-alive-on-hover placement="bottom-end" trigger="hover">
|
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
|
<n-tag size="small" type="primary">v{{ redisVersion }}</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-tooltip>
|
||||||
|
<n-tooltip v-if="redisMode">
|
||||||
|
Mode
|
||||||
|
<template #trigger>
|
||||||
|
<n-tag size="small" type="primary">{{ redisMode }}</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-tooltip>
|
||||||
|
<n-tooltip v-if="role">
|
||||||
|
Role
|
||||||
|
<template #trigger>
|
||||||
|
<n-tag size="small" type="primary">{{ role }}</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-tooltip>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-popover keep-alive-on-hover placement="bottom-end" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
:loading="pageState.loading"
|
||||||
|
:type="isLoading ? 'primary' : 'default'"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
tertiary
|
||||||
|
@click="refreshInfo(true)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :size="props.size">
|
||||||
|
<refresh
|
||||||
|
:class="{
|
||||||
|
'auto-rotate': pageState.autoRefresh || isLoading,
|
||||||
|
}"
|
||||||
|
:color="pageState.autoRefresh ? themeVars.primaryColor : undefined"
|
||||||
|
:stroke-width="pageState.autoRefresh ? 6 : 3" />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<auto-refresh-form
|
||||||
|
v-model:interval="pageState.refreshInterval"
|
||||||
|
v-model:on="pageState.autoRefresh"
|
||||||
|
:default-value="5"
|
||||||
|
:loading="pageState.autoLoading"
|
||||||
|
@toggle="onToggleRefresh" />
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
<n-spin :show="pageState.loading">
|
||||||
|
<n-grid style="min-width: 500px" x-gap="5">
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic :label="$t('status.uptime')" :value="uptime.value">
|
||||||
|
<template #suffix>{{ $t(uptime.unit) }}</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic
|
||||||
|
:label="$t('status.connected_clients')"
|
||||||
|
:value="get(serverInfo, 'Clients.connected_clients', '0')" />
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic :value="totalKeys">
|
||||||
|
<template #label>
|
||||||
|
{{ $t('status.total_keys') }}
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="6">
|
||||||
|
<n-statistic :label="$t('status.memory_used')" :value="usedMemory[0]">
|
||||||
|
<template #suffix>{{ usedMemory[1] }}</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-spin>
|
||||||
|
</n-card>
|
||||||
|
<n-card class="flex-item-expand" content-style="padding: 0; height: 100%;" embedded style="overflow: hidden">
|
||||||
|
<n-tabs
|
||||||
|
v-model:value="tabVal"
|
||||||
|
:tabs-padding="20"
|
||||||
|
pane-style="padding: 10px; box-sizing: border-box; display: flex; flex-direction: column; flex-grow: 1;"
|
||||||
|
size="large"
|
||||||
|
style="height: 100%; overflow: hidden"
|
||||||
|
type="line">
|
||||||
|
<template #suffix>
|
||||||
|
<div v-if="tabVal === 'info'" style="padding-right: 10px">
|
||||||
|
<n-input v-model:value="envFilter.keyword" clearable placeholder="">
|
||||||
|
<template #prefix>
|
||||||
|
<icon-button :icon="Filter" size="18" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- environment tab pane -->
|
||||||
|
<n-tab-pane :tab="$t('status.env_info')" name="info">
|
||||||
|
<n-space :wrap="false" :wrap-item="false" class="flex-item-expand">
|
||||||
|
<n-space align="end" item-style="padding: 0 5px;" vertical>
|
||||||
<n-button
|
<n-button
|
||||||
:loading="pageState.loading"
|
v-for="(v, k) in serverInfo"
|
||||||
:type="isLoading ? 'primary' : 'default'"
|
:key="k"
|
||||||
circle
|
:disabled="isEmpty(v)"
|
||||||
|
:focusable="false"
|
||||||
|
:type="envFilter.group === k ? 'primary' : 'default'"
|
||||||
|
secondary
|
||||||
size="small"
|
size="small"
|
||||||
tertiary
|
@click="onFilterGroup(k)">
|
||||||
@click="refreshInfo(true)">
|
<span style="min-width: 80px">{{ k }}</span>
|
||||||
<template #icon>
|
|
||||||
<n-icon :size="props.size">
|
|
||||||
<refresh
|
|
||||||
:class="{
|
|
||||||
'auto-rotate': pageState.autoRefresh || isLoading,
|
|
||||||
}"
|
|
||||||
:color="pageState.autoRefresh ? themeVars.primaryColor : undefined"
|
|
||||||
:stroke-width="pageState.autoRefresh ? 6 : 3" />
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</n-space>
|
||||||
<auto-refresh-form
|
<n-data-table
|
||||||
v-model:interval="pageState.refreshInterval"
|
:columns="[
|
||||||
v-model:on="pageState.autoRefresh"
|
{
|
||||||
:default-value="5"
|
title: $t('common.key'),
|
||||||
:loading="pageState.autoLoading"
|
key: 'key',
|
||||||
@toggle="onToggleRefresh" />
|
defaultSortOrder: 'ascend',
|
||||||
</n-popover>
|
minWidth: 80,
|
||||||
</template>
|
titleAlign: 'center',
|
||||||
<n-spin :show="pageState.loading">
|
filterOptionValue: envFilter.keyword,
|
||||||
<n-grid style="min-width: 500px" x-gap="5">
|
filter(value, row) {
|
||||||
<n-gi :span="6">
|
return !!~row.key.indexOf(value.toString())
|
||||||
<n-statistic :label="$t('status.uptime')" :value="uptime[0]">
|
|
||||||
<template #suffix>{{ $t(uptime[1]) }}</template>
|
|
||||||
</n-statistic>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="6">
|
|
||||||
<n-statistic
|
|
||||||
:label="$t('status.connected_clients')"
|
|
||||||
:value="get(serverInfo, 'Clients.connected_clients', 0)" />
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="6">
|
|
||||||
<n-statistic :value="totalKeys">
|
|
||||||
<template #label>
|
|
||||||
{{ $t('status.total_keys') }}
|
|
||||||
</template>
|
|
||||||
</n-statistic>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="6">
|
|
||||||
<n-statistic :label="$t('status.memory_used')" :value="usedMemory[0]">
|
|
||||||
<template #suffix>{{ usedMemory[1] }}</template>
|
|
||||||
</n-statistic>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
</n-spin>
|
|
||||||
</n-card>
|
|
||||||
<n-card :title="$t('status.all_info')" embedded>
|
|
||||||
<template #header-extra>
|
|
||||||
<n-input v-model:value="infoFilter" clearable placeholder="">
|
|
||||||
<template #prefix>
|
|
||||||
<icon-button :icon="Filter" size="18" />
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</template>
|
|
||||||
<n-spin :show="pageState.loading">
|
|
||||||
<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-data-table
|
|
||||||
:columns="[
|
|
||||||
{
|
|
||||||
title: $t('common.key'),
|
|
||||||
key: 'key',
|
|
||||||
defaultSortOrder: 'ascend',
|
|
||||||
sorter: 'default',
|
|
||||||
minWidth: 100,
|
|
||||||
filterOptionValue: infoFilter,
|
|
||||||
filter(value, row) {
|
|
||||||
return !!~row.key.indexOf(value.toString())
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ title: $t('common.value'), key: 'value' },
|
},
|
||||||
]"
|
{ title: $t('common.value'), titleAlign: 'center', key: 'value' },
|
||||||
:data="map(v, (value, key) => ({ value, key }))" />
|
]"
|
||||||
</n-tab-pane>
|
:data="env"
|
||||||
</n-tabs>
|
:loading="pageState.loading"
|
||||||
</n-spin>
|
:single-line="false"
|
||||||
</n-card>
|
class="flex-item-expand"
|
||||||
</n-space>
|
flex-height
|
||||||
</n-scrollbar>
|
striped />
|
||||||
|
</n-space>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- activity tab pane -->
|
||||||
|
<n-tab-pane
|
||||||
|
:tab="$t('status.activity_status')"
|
||||||
|
class="line-chart"
|
||||||
|
display-directive="show:lazy"
|
||||||
|
name="activity">
|
||||||
|
<div class="line-chart">
|
||||||
|
<div class="line-chart-item">
|
||||||
|
<Line ref="cmdRateRef" :data="cmdRate" :options="chartOption" />
|
||||||
|
</div>
|
||||||
|
<div class="line-chart-item">
|
||||||
|
<Line :data="connectedClients" :options="chartOption" />
|
||||||
|
</div>
|
||||||
|
<div class="line-chart-item">
|
||||||
|
<Line :data="memoryUsage" :options="byteChartOption" />
|
||||||
|
</div>
|
||||||
|
<div class="line-chart-item">
|
||||||
|
<Line :data="networkRate" :options="byteChartOption" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped>
|
||||||
|
@import '@/styles/content';
|
||||||
|
|
||||||
|
.line-chart {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -359,7 +359,12 @@
|
||||||
"connected_clients": "Clients",
|
"connected_clients": "Clients",
|
||||||
"total_keys": "Keys",
|
"total_keys": "Keys",
|
||||||
"memory_used": "Memory",
|
"memory_used": "Memory",
|
||||||
"all_info": "Information"
|
"all_info": "Information",
|
||||||
|
"env_info": "Environment",
|
||||||
|
"activity_status": "Activity",
|
||||||
|
"act_cmd": "Commands Per Second",
|
||||||
|
"act_network_input": "Network Input",
|
||||||
|
"act_network_output": "Network Output"
|
||||||
},
|
},
|
||||||
"slog": {
|
"slog": {
|
||||||
"title": "Slow Log",
|
"title": "Slow Log",
|
||||||
|
|
|
@ -290,8 +290,7 @@
|
||||||
"uptime": "Tempo de Atividade",
|
"uptime": "Tempo de Atividade",
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"slog": {
|
"slog": {
|
||||||
"title": "Registro de Operações Lentas",
|
"title": "Registro de Operações Lentas",
|
||||||
|
|
|
@ -359,7 +359,12 @@
|
||||||
"connected_clients": "已连客户端",
|
"connected_clients": "已连客户端",
|
||||||
"total_keys": "键总数",
|
"total_keys": "键总数",
|
||||||
"memory_used": "内存使用",
|
"memory_used": "内存使用",
|
||||||
"all_info": "全部信息"
|
"all_info": "全部信息",
|
||||||
|
"env_info": "运行环境",
|
||||||
|
"activity_status": "活动状态",
|
||||||
|
"act_cmd": "命令执行数/秒",
|
||||||
|
"act_network_input": "网络输入",
|
||||||
|
"act_network_output": "网络输出"
|
||||||
},
|
},
|
||||||
"slog": {
|
"slog": {
|
||||||
"title": "慢日志",
|
"title": "慢日志",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { setupDiscreteApi } from '@/utils/discrete.js'
|
||||||
import usePreferencesStore from 'stores/preferences.js'
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
import { loadEnvironment } from '@/utils/platform.js'
|
import { loadEnvironment } from '@/utils/platform.js'
|
||||||
import { setupMonaco } from '@/utils/monaco.js'
|
import { setupMonaco } from '@/utils/monaco.js'
|
||||||
|
import { setupChart } from '@/utils/chart.js'
|
||||||
|
|
||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
@ -21,6 +22,7 @@ async function setupApp() {
|
||||||
|
|
||||||
await loadEnvironment()
|
await loadEnvironment()
|
||||||
setupMonaco()
|
setupMonaco()
|
||||||
|
setupChart()
|
||||||
const prefStore = usePreferencesStore()
|
const prefStore = usePreferencesStore()
|
||||||
await prefStore.loadPreferences()
|
await prefStore.loadPreferences()
|
||||||
await setupDiscreteApi()
|
await setupDiscreteApi()
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
/**
|
||||||
|
* convert byte value
|
||||||
|
* @param {number} bytes
|
||||||
|
* @param {number} decimals
|
||||||
|
* @return {{unit: string, value: number}}
|
||||||
|
*/
|
||||||
|
export const convertBytes = (bytes, decimals = 2) => {
|
||||||
|
if (bytes <= 0) {
|
||||||
|
return {
|
||||||
|
value: 0,
|
||||||
|
unit: sizes[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
const j = Math.min(i, sizes.length - 1)
|
||||||
|
return {
|
||||||
|
value: parseFloat((bytes / Math.pow(k, j)).toFixed(decimals)),
|
||||||
|
unit: sizes[j],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} bytes
|
||||||
|
* @param {number} decimals
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const formatBytes = (bytes, decimals = 2) => {
|
||||||
|
const res = convertBytes(bytes, decimals)
|
||||||
|
return res.value + ' ' + res.unit
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {
|
||||||
|
CategoryScale,
|
||||||
|
Chart as ChartJS,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from 'chart.js'
|
||||||
|
|
||||||
|
export const setupChart = () => {
|
||||||
|
ChartJS.register(Title, Tooltip, LineElement, CategoryScale, LinearScale, PointElement, Legend, Filler)
|
||||||
|
}
|
Loading…
Reference in New Issue