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 VirtualFolders from '../views/VirtualFolders.vue'
|
||||||
import Quota from '../views/Quota.vue'
|
import Quota from '../views/Quota.vue'
|
||||||
import ACL from '../views/ACL.vue'
|
import ACL from '../views/ACL.vue'
|
||||||
|
import Monitor from '../views/Monitor.vue'
|
||||||
import FilePreview from '../views/FilePreview.vue'
|
import FilePreview from '../views/FilePreview.vue'
|
||||||
import Home from '../views/Home.vue'
|
import Home from '../views/Home.vue'
|
||||||
import Dashboard from '../views/Dashboard.vue'
|
import Dashboard from '../views/Dashboard.vue'
|
||||||
@@ -12,7 +13,6 @@ import Config from '../views/Config.vue'
|
|||||||
import Diagnostic from '../views/Diagnostic.vue'
|
import Diagnostic from '../views/Diagnostic.vue'
|
||||||
import Management from '../views/Management.vue'
|
import Management from '../views/Management.vue'
|
||||||
import Health from '../views/Health.vue'
|
import Health from '../views/Health.vue'
|
||||||
import Monitor from '../views/Monitor.vue'
|
|
||||||
import Backup from '../views/Backup.vue'
|
import Backup from '../views/Backup.vue'
|
||||||
import Users from '../views/Users.vue'
|
import Users from '../views/Users.vue'
|
||||||
import Shares from '../views/Shares.vue'
|
import Shares from '../views/Shares.vue'
|
||||||
@@ -48,6 +48,11 @@ const routes = [
|
|||||||
name: 'ACL',
|
name: 'ACL',
|
||||||
component: ACL
|
component: ACL
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/monitor',
|
||||||
|
name: 'Monitor',
|
||||||
|
component: Monitor
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/filepreview',
|
path: '/filepreview',
|
||||||
name: 'FilePreview',
|
name: 'FilePreview',
|
||||||
|
|||||||
@@ -1,205 +1,170 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
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 autoRefresh = ref(true)
|
||||||
const refreshInterval = ref(5)
|
|
||||||
let refreshTimer = null
|
|
||||||
|
|
||||||
const loadMonitorData = async () => {
|
const loadServices = async () => {
|
||||||
try {
|
try {
|
||||||
monitorData.value = await getMonitorData()
|
const result = await invoke('get_all_services_status')
|
||||||
|
services.value = result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(`Failed to load monitor data: ${error}`)
|
console.error('Failed to load services:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startAutoRefresh = () => {
|
const loadStats = async () => {
|
||||||
if (refreshTimer) {
|
try {
|
||||||
clearInterval(refreshTimer)
|
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) {
|
||||||
refreshTimer = setInterval(async () => {
|
console.error('Failed to load stats:', error)
|
||||||
await loadMonitorData()
|
|
||||||
}, refreshInterval.value * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopAutoRefresh = () => {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleAutoRefresh = () => {
|
const refreshAll = async () => {
|
||||||
if (autoRefresh.value) {
|
loading.value = true
|
||||||
startAutoRefresh()
|
await Promise.all([
|
||||||
} else {
|
loadServices(),
|
||||||
stopAutoRefresh()
|
loadStats()
|
||||||
}
|
])
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatBytes = (bytes) => {
|
const serviceStatusColor = (status) => {
|
||||||
if (bytes === 0) return '0 B'
|
switch (status) {
|
||||||
const k = 1024
|
case 'running': return 'success'
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
case 'stopped': return 'danger'
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
case 'error': return 'warning'
|
||||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
default: return 'info'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadMonitorData()
|
await refreshAll()
|
||||||
if (autoRefresh.value) {
|
if (autoRefresh.value) {
|
||||||
startAutoRefresh()
|
refreshInterval.value = setInterval(refreshAll, 5000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopAutoRefresh()
|
if (refreshInterval.value) {
|
||||||
|
clearInterval(refreshInterval.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="monitor-container">
|
<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>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>Monitor Dashboard</h2>
|
<span>Services Status</span>
|
||||||
<div class="refresh-controls">
|
<el-tag :type="autoRefresh ? 'success' : 'info'" size="small">
|
||||||
<el-switch v-model="autoRefresh" @change="toggleAutoRefresh" />
|
{{ autoRefresh ? 'Auto Refresh (5s)' : 'Manual Refresh' }}
|
||||||
<span>Auto Refresh ({{ refreshInterval }}s)</span>
|
</el-tag>
|
||||||
<el-button type="primary" size="small" @click="loadMonitorData">
|
|
||||||
Refresh Now
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="monitorData">
|
<el-table :data="services" v-loading="loading" stripe style="width: 100%">
|
||||||
<el-row :gutter="20" class="system-row">
|
<el-table-column prop="name" label="Service" min-width="150">
|
||||||
<el-col :span="6">
|
<template #default="{ row }">
|
||||||
<el-card shadow="hover">
|
<el-icon style="margin-right: 5px"><Monitor /></el-icon>
|
||||||
<div class="metric-card">
|
<span>{{ row.name }}</span>
|
||||||
<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>
|
</template>
|
||||||
<el-descriptions :column="1" border>
|
</el-table-column>
|
||||||
<el-descriptions-item label="Total Files">
|
<el-table-column prop="status" label="Status" width="120">
|
||||||
{{ monitorData.file_system.total_files }}
|
<template #default="{ row }">
|
||||||
</el-descriptions-item>
|
<el-tag :type="serviceStatusColor(row.status)" size="small">
|
||||||
<el-descriptions-item label="Total Size">
|
<el-icon style="margin-right: 5px">
|
||||||
{{ formatBytes(monitorData.file_system.total_size) }}
|
<CircleCheck v-if="row.status === 'running'" />
|
||||||
</el-descriptions-item>
|
<CircleClose v-else />
|
||||||
<el-descriptions-item label="File Tree Size">
|
</el-icon>
|
||||||
{{ formatBytes(monitorData.file_system.file_tree_size) }}
|
{{ row.status }}
|
||||||
</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>
|
|
||||||
</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>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="uptime_seconds" label="Uptime">
|
<el-table-column prop="port" label="Port" width="100" />
|
||||||
<template #default="scope">
|
<el-table-column prop="uptime" label="Uptime" min-width="150" />
|
||||||
{{ Math.floor(scope.row.uptime_seconds / 3600) }}h {{ Math.floor(scope.row.uptime_seconds % 3600 / 60) }}m
|
<el-table-column prop="connections" label="Connections" width="120" />
|
||||||
</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-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-empty v-else description="No monitor data available" />
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -207,48 +172,83 @@ onUnmounted(() => {
|
|||||||
padding: 20px;
|
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 {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
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>
|
</style>
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import DashboardView from './Dashboard.vue'
|
import DashboardView from './Dashboard.vue'
|
||||||
import UsersView from './Users.vue'
|
import UsersView from './Users.vue'
|
||||||
import SharesView from './Shares.vue'
|
import SharesView from './Shares.vue'
|
||||||
|
import MonitorView from './Monitor.vue'
|
||||||
|
|
||||||
const activeTab = ref('dashboard')
|
const activeTab = ref('dashboard')
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ const currentTab = computed(() => {
|
|||||||
case 'dashboard': return DashboardView
|
case 'dashboard': return DashboardView
|
||||||
case 'users': return UsersView
|
case 'users': return UsersView
|
||||||
case 'shares': return SharesView
|
case 'shares': return SharesView
|
||||||
case 'monitor': return null
|
case 'monitor': return MonitorView
|
||||||
default: return DashboardView
|
default: return DashboardView
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -53,11 +54,7 @@ const currentTab = computed(() => {
|
|||||||
|
|
||||||
<div class="webadmin-content">
|
<div class="webadmin-content">
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<component :is="currentTab" v-if="currentTab" />
|
<component :is="currentTab" />
|
||||||
<div v-else class="monitor-placeholder">
|
|
||||||
<el-icon :size="50"><Monitor /></el-icon>
|
|
||||||
<p>Monitor 功能开发中...</p>
|
|
||||||
</div>
|
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,18 +110,4 @@ const currentTab = computed(() => {
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
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>
|
</style>
|
||||||
Reference in New Issue
Block a user