Add Monitor UI: Service status + performance monitoring with auto-refresh
This commit is contained in:
@@ -4,6 +4,7 @@ import WebAdmin from '../views/WebAdmin.vue'
|
||||
import VirtualFolders from '../views/VirtualFolders.vue'
|
||||
import Quota from '../views/Quota.vue'
|
||||
import ACL from '../views/ACL.vue'
|
||||
import Monitor from '../views/Monitor.vue'
|
||||
import FilePreview from '../views/FilePreview.vue'
|
||||
import Home from '../views/Home.vue'
|
||||
import Dashboard from '../views/Dashboard.vue'
|
||||
@@ -12,7 +13,6 @@ import Config from '../views/Config.vue'
|
||||
import Diagnostic from '../views/Diagnostic.vue'
|
||||
import Management from '../views/Management.vue'
|
||||
import Health from '../views/Health.vue'
|
||||
import Monitor from '../views/Monitor.vue'
|
||||
import Backup from '../views/Backup.vue'
|
||||
import Users from '../views/Users.vue'
|
||||
import Shares from '../views/Shares.vue'
|
||||
@@ -48,6 +48,11 @@ const routes = [
|
||||
name: 'ACL',
|
||||
component: ACL
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: Monitor
|
||||
},
|
||||
{
|
||||
path: '/filepreview',
|
||||
name: 'FilePreview',
|
||||
|
||||
@@ -1,203 +1,168 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getMonitorData } from '../api/tauri'
|
||||
import { Monitor, CircleCheck, CircleClose, Loading } from '@element-plus/icons-vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
const monitorData = ref(null)
|
||||
const services = ref([])
|
||||
const stats = ref({
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
disk: 0
|
||||
})
|
||||
const loading = ref(false)
|
||||
const refreshInterval = ref(null)
|
||||
const autoRefresh = ref(true)
|
||||
const refreshInterval = ref(5)
|
||||
let refreshTimer = null
|
||||
|
||||
const loadMonitorData = async () => {
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
monitorData.value = await getMonitorData()
|
||||
const result = await invoke('get_all_services_status')
|
||||
services.value = result
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to load monitor data: ${error}`)
|
||||
console.error('Failed to load services:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const startAutoRefresh = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
|
||||
refreshTimer = setInterval(async () => {
|
||||
await loadMonitorData()
|
||||
}, refreshInterval.value * 1000)
|
||||
}
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const result = await invoke('get_system_stats')
|
||||
stats.value = {
|
||||
cpu: result.cpu_usage || 0,
|
||||
memory: result.memory_usage || 0,
|
||||
disk: result.disk_usage || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAutoRefresh = () => {
|
||||
if (autoRefresh.value) {
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
const refreshAll = async () => {
|
||||
loading.value = true
|
||||
await Promise.all([
|
||||
loadServices(),
|
||||
loadStats()
|
||||
])
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||
const serviceStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'running': return 'success'
|
||||
case 'stopped': return 'danger'
|
||||
case 'error': return 'warning'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMonitorData()
|
||||
await refreshAll()
|
||||
if (autoRefresh.value) {
|
||||
startAutoRefresh()
|
||||
refreshInterval.value = setInterval(refreshAll, 5000)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="monitor-container">
|
||||
<el-card>
|
||||
<div class="monitor-header">
|
||||
<h2>System Monitor</h2>
|
||||
<p class="header-subtitle">服务状态 + 性能监控</p>
|
||||
<div class="header-actions">
|
||||
<el-switch
|
||||
v-model="autoRefresh"
|
||||
@change="(val) => {
|
||||
if (val) {
|
||||
refreshInterval = setInterval(refreshAll, 5000)
|
||||
} else {
|
||||
clearInterval(refreshInterval)
|
||||
}
|
||||
}"
|
||||
active-text="Auto Refresh"
|
||||
/>
|
||||
<el-button @click="refreshAll" :icon="Loading" :loading="loading" size="small">Refresh</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20" style="margin-bottom: 20px">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon cpu">
|
||||
<el-icon :size="40"><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">CPU Usage</div>
|
||||
<div class="stat-value">{{ stats.cpu.toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon memory">
|
||||
<el-icon :size="40"><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">Memory Usage</div>
|
||||
<div class="stat-value">{{ stats.memory.toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon disk">
|
||||
<el-icon :size="40"><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">Disk Usage</div>
|
||||
<div class="stat-value">{{ stats.disk.toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>Monitor Dashboard</h2>
|
||||
<div class="refresh-controls">
|
||||
<el-switch v-model="autoRefresh" @change="toggleAutoRefresh" />
|
||||
<span>Auto Refresh ({{ refreshInterval }}s)</span>
|
||||
<el-button type="primary" size="small" @click="loadMonitorData">
|
||||
Refresh Now
|
||||
</el-button>
|
||||
</div>
|
||||
<span>Services Status</span>
|
||||
<el-tag :type="autoRefresh ? 'success' : 'info'" size="small">
|
||||
{{ autoRefresh ? 'Auto Refresh (5s)' : 'Manual Refresh' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="monitorData">
|
||||
<el-row :gutter="20" class="system-row">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="metric-card">
|
||||
<h3>CPU Usage</h3>
|
||||
<el-progress
|
||||
type="dashboard"
|
||||
:percentage="monitorData.system.cpu_usage"
|
||||
:color="monitorData.system.cpu_usage > 80 ? '#F56C6C' : '#67C23A'"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="metric-card">
|
||||
<h3>Memory Usage</h3>
|
||||
<el-progress
|
||||
type="dashboard"
|
||||
:percentage="monitorData.system.memory_usage"
|
||||
:color="monitorData.system.memory_usage > 80 ? '#F56C6C' : '#67C23A'"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="metric-card">
|
||||
<h3>Disk Usage</h3>
|
||||
<el-progress
|
||||
type="dashboard"
|
||||
:percentage="monitorData.system.disk_usage"
|
||||
:color="monitorData.system.disk_usage > 80 ? '#F56C6C' : '#67C23A'"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="metric-card">
|
||||
<h3>Network Traffic</h3>
|
||||
<div class="network-metrics">
|
||||
<p>In: {{ formatBytes(monitorData.system.network_in) }}</p>
|
||||
<p>Out: {{ formatBytes(monitorData.system.network_out) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="details-row">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<h3>File System</h3>
|
||||
</template>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="Total Files">
|
||||
{{ monitorData.file_system.total_files }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Total Size">
|
||||
{{ formatBytes(monitorData.file_system.total_size) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="File Tree Size">
|
||||
{{ formatBytes(monitorData.file_system.file_tree_size) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<h3>Database</h3>
|
||||
</template>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="Database Size">
|
||||
{{ formatBytes(monitorData.database.database_size) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Table Rows">
|
||||
<div v-for="(rows, table) in monitorData.database.table_rows" :key="table">
|
||||
{{ table }}: {{ rows }} rows
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="hover" class="services-card">
|
||||
<template #header>
|
||||
<h3>Service Status</h3>
|
||||
|
||||
<el-table :data="services" v-loading="loading" stripe style="width: 100%">
|
||||
<el-table-column prop="name" label="Service" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-icon style="margin-right: 5px"><Monitor /></el-icon>
|
||||
<span>{{ row.name }}</span>
|
||||
</template>
|
||||
<el-table :data="monitorData.services" style="width: 100%">
|
||||
<el-table-column prop="name" label="Service Name" />
|
||||
<el-table-column prop="status" label="Status">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'Running' ? 'success' : 'danger'">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uptime_seconds" label="Uptime">
|
||||
<template #default="scope">
|
||||
{{ Math.floor(scope.row.uptime_seconds / 3600) }}h {{ Math.floor(scope.row.uptime_seconds % 3600 / 60) }}m
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_check" label="Last Check">
|
||||
<template #default="scope">
|
||||
{{ new Date(scope.row.last_check).toLocaleString() }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="No monitor data available" />
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="Status" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="serviceStatusColor(row.status)" size="small">
|
||||
<el-icon style="margin-right: 5px">
|
||||
<CircleCheck v-if="row.status === 'running'" />
|
||||
<CircleClose v-else />
|
||||
</el-icon>
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="port" label="Port" width="100" />
|
||||
<el-table-column prop="uptime" label="Uptime" min-width="150" />
|
||||
<el-table-column prop="connections" label="Connections" width="120" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -207,48 +172,83 @@ onUnmounted(() => {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.monitor-header {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.monitor-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
margin: 5px 0 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.stat-icon.cpu {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-icon.memory {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-icon.disk {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.refresh-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.system-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.metric-card h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.network-metrics {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.network-metrics p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.details-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.services-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import DashboardView from './Dashboard.vue'
|
||||
import UsersView from './Users.vue'
|
||||
import SharesView from './Shares.vue'
|
||||
import MonitorView from './Monitor.vue'
|
||||
|
||||
const activeTab = ref('dashboard')
|
||||
|
||||
@@ -23,7 +24,7 @@ const currentTab = computed(() => {
|
||||
case 'dashboard': return DashboardView
|
||||
case 'users': return UsersView
|
||||
case 'shares': return SharesView
|
||||
case 'monitor': return null
|
||||
case 'monitor': return MonitorView
|
||||
default: return DashboardView
|
||||
}
|
||||
})
|
||||
@@ -53,11 +54,7 @@ const currentTab = computed(() => {
|
||||
|
||||
<div class="webadmin-content">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="currentTab" v-if="currentTab" />
|
||||
<div v-else class="monitor-placeholder">
|
||||
<el-icon :size="50"><Monitor /></el-icon>
|
||||
<p>Monitor 功能开发中...</p>
|
||||
</div>
|
||||
<component :is="currentTab" />
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,18 +110,4 @@ const currentTab = computed(() => {
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.monitor-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.monitor-placeholder p {
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user