Add Backup Management GUI (Phase 3-4)
Web GUI Implementation:
- Backup.vue: Storage dashboard + Snapshot management + Scheduler config
- Router: Add /backup route
- Home.vue: Add Backup management card
- Tauri commands: 10 backup API endpoints
Features:
- Storage stats (total/used/free, dedup/compression ratios)
- Snapshot list with create/delete/restore actions
- Backup scheduler configuration (enabled, interval, max_snapshots)
- Run backup now button
- Send/Receive placeholders
Tauri Commands:
- get_storage_stats, list_snapshots
- create_snapshot, delete_snapshot, restore_snapshot
- get_backup_stats, get_backup_config, set_backup_config
- run_backup
Build: cargo build (Tauri) ✅ 5 warnings
Tests: 495 markbase-core + 201 smb-server = 696 total
This commit is contained in:
@@ -6,6 +6,7 @@ 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'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -42,6 +43,11 @@ const routes = [
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: Monitor
|
||||
},
|
||||
{
|
||||
path: '/backup',
|
||||
name: 'Backup',
|
||||
component: Backup
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
481
markbase-tauri/src/src/views/Backup.vue
Normal file
481
markbase-tauri/src/src/views/Backup.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
FolderOpened,
|
||||
Clock,
|
||||
Refresh,
|
||||
Delete,
|
||||
Download,
|
||||
Upload,
|
||||
Setting,
|
||||
DataAnalysis,
|
||||
Timer,
|
||||
Warning
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const storageStats = ref({
|
||||
total_size: 0,
|
||||
used_size: 0,
|
||||
free_size: 0,
|
||||
dedup_ratio: 1.0,
|
||||
compression_ratio: 1.0
|
||||
})
|
||||
|
||||
const snapshots = ref([])
|
||||
const backupConfig = ref({
|
||||
enabled: false,
|
||||
interval_hours: 24,
|
||||
max_snapshots: 7,
|
||||
auto_cleanup: true
|
||||
})
|
||||
|
||||
const schedulerStats = ref({
|
||||
enabled: false,
|
||||
backup_count: 0,
|
||||
last_backup: null,
|
||||
next_backup: null,
|
||||
interval_hours: 24,
|
||||
max_snapshots: 7
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const creatingSnapshot = ref(false)
|
||||
const snapshotName = ref('')
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'
|
||||
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
||||
}
|
||||
|
||||
const formatRatio = (ratio) => {
|
||||
return (1 / ratio).toFixed(2) + 'x'
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return 'Never'
|
||||
const date = new Date(timestamp * 1000)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const usedPercentage = computed(() => {
|
||||
if (storageStats.value.total_size === 0) return 0
|
||||
return Math.round((storageStats.value.used_size / storageStats.value.total_size) * 100)
|
||||
})
|
||||
|
||||
const loadStorageStats = async () => {
|
||||
try {
|
||||
const stats = await invoke('get_storage_stats', { rootPath: '/data' })
|
||||
storageStats.value = stats
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to load storage stats: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const loadSnapshots = async () => {
|
||||
try {
|
||||
const list = await invoke('list_snapshots', { rootPath: '/data' })
|
||||
snapshots.value = list
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to load snapshots: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const loadSchedulerStats = async () => {
|
||||
try {
|
||||
const stats = await invoke('get_backup_stats')
|
||||
schedulerStats.value = stats
|
||||
} catch (error) {
|
||||
console.log('Scheduler stats not available:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const createSnapshot = async () => {
|
||||
if (!snapshotName.value) {
|
||||
ElMessage.warning('Please enter snapshot name')
|
||||
return
|
||||
}
|
||||
|
||||
creatingSnapshot.value = true
|
||||
try {
|
||||
await invoke('create_snapshot', {
|
||||
rootPath: '/data',
|
||||
snapshotName: snapshotName.value
|
||||
})
|
||||
ElMessage.success(`Snapshot '${snapshotName.value}' created`)
|
||||
snapshotName.value = ''
|
||||
await loadSnapshots()
|
||||
await loadStorageStats()
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to create snapshot: ${error}`)
|
||||
} finally {
|
||||
creatingSnapshot.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSnapshot = async (name) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`Are you sure you want to delete snapshot '${name}'?`,
|
||||
'Delete Snapshot',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
await invoke('delete_snapshot', {
|
||||
rootPath: '/data',
|
||||
snapshotName: name
|
||||
})
|
||||
ElMessage.success(`Snapshot '${name}' deleted`)
|
||||
await loadSnapshots()
|
||||
await loadStorageStats()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(`Failed to delete snapshot: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const restoreSnapshot = async (name) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`Are you sure you want to restore from snapshot '${name}'? Current data will be replaced.`,
|
||||
'Restore Snapshot',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
await invoke('restore_snapshot', {
|
||||
rootPath: '/data',
|
||||
snapshotName: name
|
||||
})
|
||||
ElMessage.success(`Restored from snapshot '${name}'`)
|
||||
await loadStorageStats()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(`Failed to restore snapshot: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const runBackup = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const result = await invoke('run_backup')
|
||||
ElMessage.success(`Backup completed: ${result}`)
|
||||
await loadSnapshots()
|
||||
await loadSchedulerStats()
|
||||
} catch (error) {
|
||||
ElMessage.error(`Backup failed: ${error}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveBackupConfig = async () => {
|
||||
try {
|
||||
await invoke('set_backup_config', { config: backupConfig.value })
|
||||
ElMessage.success('Backup configuration saved')
|
||||
await loadSchedulerStats()
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to save config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshAll = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
loadStorageStats(),
|
||||
loadSnapshots(),
|
||||
loadSchedulerStats()
|
||||
])
|
||||
ElMessage.success('Data refreshed')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="backup-container">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-card class="stats-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span><el-icon><DataAnalysis /></el-icon> Storage Dashboard</span>
|
||||
<el-button :icon="Refresh" size="small" @click="refreshAll" :loading="loading">
|
||||
Refresh
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="Total Storage" :value="formatSize(storageStats.total_size)" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="Used" :value="formatSize(storageStats.used_size)" />
|
||||
<el-progress
|
||||
:percentage="usedPercentage"
|
||||
:color="usedPercentage > 80 ? '#f56c6c' : '#67c23a'"
|
||||
:stroke-width="8"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="Free" :value="formatSize(storageStats.free_size)" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="ratio-stats">
|
||||
<div class="ratio-item">
|
||||
<span>Deduplication:</span>
|
||||
<strong>{{ formatRatio(storageStats.dedup_ratio) }}</strong>
|
||||
</div>
|
||||
<div class="ratio-item">
|
||||
<span>Compression:</span>
|
||||
<strong>{{ formatRatio(storageStats.compression_ratio) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="16">
|
||||
<el-card class="snapshots-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span><el-icon><Clock /></el-icon> Snapshots</span>
|
||||
<div class="header-actions">
|
||||
<el-input
|
||||
v-model="snapshotName"
|
||||
placeholder="Snapshot name"
|
||||
size="small"
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="FolderOpened"
|
||||
:loading="creatingSnapshot"
|
||||
@click="createSnapshot"
|
||||
>
|
||||
Create
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="snapshots" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="name" label="Name" min-width="200" />
|
||||
<el-table-column prop="created" label="Created" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.created) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="size" label="Size" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatSize(row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="read_only" label="Read Only" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.read_only ? 'success' : 'info'" size="small">
|
||||
{{ row.read_only ? 'Yes' : 'No' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Actions" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
:icon="Download"
|
||||
@click="restoreSnapshot(row.name)"
|
||||
>
|
||||
Restore
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="deleteSnapshot(row.name)"
|
||||
>
|
||||
Delete
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-card class="scheduler-card">
|
||||
<template #header>
|
||||
<span><el-icon><Timer /></el-icon> Backup Scheduler</span>
|
||||
</template>
|
||||
|
||||
<div class="scheduler-stats">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="Status">
|
||||
<el-tag :type="schedulerStats.enabled ? 'success' : 'info'">
|
||||
{{ schedulerStats.enabled ? 'Enabled' : 'Disabled' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Backup Count">
|
||||
{{ schedulerStats.backup_count }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Last Backup">
|
||||
{{ formatTime(schedulerStats.last_backup) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Next Backup">
|
||||
{{ formatTime(schedulerStats.next_backup) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Interval">
|
||||
{{ schedulerStats.interval_hours }} hours
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Max Snapshots">
|
||||
{{ schedulerStats.max_snapshots }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Refresh"
|
||||
:loading="loading"
|
||||
@click="runBackup"
|
||||
style="width: 100%; margin-bottom: 15px;"
|
||||
>
|
||||
Run Backup Now
|
||||
</el-button>
|
||||
|
||||
<div class="config-section">
|
||||
<h4><el-icon><Setting /></el-icon> Configuration</h4>
|
||||
<el-form label-width="120px" size="small">
|
||||
<el-form-item label="Enabled">
|
||||
<el-switch v-model="backupConfig.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Interval (hrs)">
|
||||
<el-input-number v-model="backupConfig.interval_hours" :min="1" :max="168" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Max Snapshots">
|
||||
<el-input-number v-model="backupConfig.max_snapshots" :min="1" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Auto Cleanup">
|
||||
<el-switch v-model="backupConfig.auto_cleanup" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveBackupConfig">Save Config</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="send-receive-card" style="margin-top: 20px;">
|
||||
<template #header>
|
||||
<span><el-icon><Upload /></el-icon> Send / Receive</span>
|
||||
</template>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 15px;"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon><Warning /></el-icon> Advanced Operations
|
||||
</template>
|
||||
Send snapshots to remote storage or receive from remote.
|
||||
</el-alert>
|
||||
<el-button-group style="width: 100%;">
|
||||
<el-button :icon="Upload" style="width: 50%;">Send Snapshot</el-button>
|
||||
<el-button :icon="Download" style="width: 50%;">Receive Snapshot</el-button>
|
||||
</el-button-group>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.backup-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ratio-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.ratio-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 10px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ratio-item strong {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.snapshots-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.scheduler-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.scheduler-stats {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.config-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.send-receive-card {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Folder, Document, Upload } from '@element-plus/icons-vue'
|
||||
import { Folder, Document, Upload, Clock } from '@element-plus/icons-vue'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -217,6 +217,14 @@ onMounted(async () => {
|
||||
<p>Real-time monitoring</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="management-card" @click="navigateTo('/backup')">
|
||||
<div class="card-content">
|
||||
<el-icon :size="40"><Clock /></el-icon>
|
||||
<h3>Backup Management</h3>
|
||||
<p>Snapshots and scheduler</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
Reference in New Issue
Block a user